简体   繁体   中英

React setState not updating state; callback not being called

I'm trying to take the metronome I built following the "Tale of Two Clocks" tutorial and convert it into a simple React project. I figured it would be trivially easy to map the metronome's parameters (tempo, note divisions, etc.) and expand on it to build out a step sequencer (hooray for techno!). That said, I'm having trouble with a specific piece and I'm wondering why.

I've got pretty much all the salient pieces as state on the Metronome component, like so:

import React from 'react'

class Metronome extends React.Component {
  constructor (props) {
    super(props)

    this.audioContext = new (window.AudioContext || window.webkitAudioContext)()

    this.state = {
      tempo: 120.0,
      noteResolution: 0,
      isPlaying: false,
      current16thNote: 0,
      gateLength: 0.05,
      noteLength: 0.25,
      nextNoteTime: 0.0
    }

    this.lookahead = 0.1
    this.schedulerInterval = 25.0
    this.timerID = null

    this.updateStartStopText = this.updateStartStopText.bind(this)
    this.play = this.play.bind(this)
    this.scheduler = this.scheduler.bind(this)
    this.scheduleNote = this.scheduleNote.bind(this)
    this.nextNote = this.nextNote.bind(this)
  }

  updateStartStopText () {
    return this.state.isPlaying ? 'Stop' : 'Start'
  }

  play () {
    this.setState({ isPlaying: !this.state.isPlaying }, () => {
      if (this.state.isPlaying) {
        this.setState({ current16thNote: 0 }, () => {
          this.setState({
            nextNoteTime: this.audioContext.currentTime
          }, this.scheduler)
        })
      } else {
        window.clearTimeout(this.timerID)
      }
    })
  }

  scheduler () {
    while (this.state.nextNoteTime < this.audioContext.currentTime + this.lookahead) {
      this.scheduleNote(this.state.current16thNote, this.state.nextNoteTime)
      this.nextNote()
    }

    this.timerID = window.setTimeout(this.scheduler, this.schedulerInterval)
  }

  scheduleNote (beatNumber, time) {
    if ((this.state.noteResolution === 1) && (beatNumber % 2)) {
      return
    } else if ((this.state.noteResolution === 2) && (beatNumber % 4)) {
      return
    }
    const osc = this.audioContext.createOscillator()
    osc.connect(this.audioContext.destination)
    if (!(beatNumber % 16)) {
      osc.frequency.value = 220
    } else if (beatNumber % 4) {
      osc.frequency.value = 440
    } else {
      osc.frequency.value = 880
    }

    osc.start(time)
    osc.stop(time + this.state.gateLength)
  }

  nextNote () {
    var secondsPerBeat = 60.0 / this.state.tempo
    let nextTime = this.state.nextNoteTime + (secondsPerBeat * this.state.noteLength)

    this.setState({
      nextNoteTime: nextTime,
      current16thNote: (this.state.current16thNote + 1)
    }, () => {
      if (this.state.current16thNote === 16) {
        this.setState({ current16thNote: 0 })
      }
    })
  }

  render () {
    return (
      <div>
        Metronome

        <button type='button' onClick={this.play}>{this.updateStartStopText()}</button>
      </div>
    )
  }
}

export default Metronome

More or less everything plays out like you'd expect from React, swapping out vanilla JS variable reassignment/incrementing with this.setState() calls, . There are quite a few situations where I need to do calculations with an updated value in state and, knowing that setState() is asynchronous and often batches calls, I rely somewhat heavily on the optional callback that setState takes.

The one place where things seem to be going awry is in the nextNote() method. The state never updates properly: this.state.nextNoteTime never gets an updated value beyond that which it is assigned in the play() method The code as it is will never get into the callback passed to setState() called from nextNote() . If I remove nextNoteTime from the component's state and save it as a property on the component, manually updating and tracking its value, the metronome seems to work as expected, à la the end state of my metronome in vanilla JS. Technically a success, but not the answer I'm looking for.

I tried using Promises and I tried using the React Component API ( componentWillUpdate , shouldComponentUpdate , etc.) to figure out whether or not the next state is the one I'm looking for. But beyond a certain point (like 3 recursive calls of the scheduler() method), the while loop goes haywire since this.state.nextNoteTime never increases. Gah!

Thoughts?

I think it's because you have nested setState s.

Try below as play method.

play() {
  const { isPlaying } = this.state;
  const newState = { isPlaying: !isPlaying };
  if (!isPlaying) {
    Object.assign(newState, {
      current16thNote: 0,
      nextNoteTime: this.audioContext.currentTime
    });
  }

  this.setState(newState, (...args) => {
    if (!isPlaying) {
      this.scheduler(...args);
    } else {
      window.clearTimeout(this.timerID)
    }
  });
}

Remove setState nesting for nextNote as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM