0

I'm a beginner and I feel like I'm making a fundamental mistake somewhere. I'm making a simple React component to get notes to play in order using Tone JS. I can't update the notes with a button. When I click a button to check they're updated it seems the state has been changed but the repeat function still plays the 'old notes'. Where am I going wrong?

// create a synth
let synth = new Tone.Synth({
  attack: 0.5,
  decay: 0.5,
  sustain: 1,
  release: 5,
  volume: -10
}).toDestination()

const Main = () => {

  // set state with note values
  const [noteValue, setNoteValue] = useState([
    { note: 'C4', properties: ''},
    { note: 'C4', properties: ''}
  ])
  const [isPlaying, setIsPlaying] = useState(false)

  // start/stop transport
  const startStop = () => {
    if (!isPlaying) Tone.start()
    setIsPlaying(!isPlaying)
    !isPlaying ? Tone.Transport.start() : Tone.Transport.stop()
  }

  Tone.Transport.bpm.value = 140

  // song loop function - always displays the same notes after state change
  let index = 0
  function repeat(time){
    const position = index % noteValue.length
    const synthNote = noteValue[position]
    console.log(noteValue)
    synth.triggerAttackRelease(synthNote.note, time)
    index++
  }

  // set the transport on the first render
  useEffect(() => {
    Tone.Transport.scheduleRepeat((time) => { 
      repeat(time)
    }, '4n')
  },[])

// the "change note values" button doesn't change them inside the repeat function
return <>

    <button onClick={() => setNoteValue([
    { note: 'C5', properties: ''},
    { note: 'C5', properties: ''}
  ])}>Change Note Values</button>

    <button onClick={() => console.log(noteValue)}>Check note values</button>

    <button onClick={() => startStop()}>Start/Stop</button>
</>

Thank you.

1 Answers1

1

Your useEffect is executed once on first render. In that useEffect, you schedule a repeat of a function referencing the repeat function from that scope. That repeat function references noteValue, which again is the value of a variable named noteValue that exists in the scope of the first render.

Your react state value does in fact change, but since you always only reference variables from the scope of the first execution in your effect, you will not experience any of this.

To see that it is actually changing, you can add a console.log(noteValue) somewhere in your code.

To solve this, you really need to understand the concept of scope and closures. I recommend reading https://whatthefuck.is/closure .

Possible solution:

One possible solution would be to have scheduleRepeat return a unschedule method (you need that anyways, otherwise sound will keep playing after your component unmounted). In that case:

  useEffect(() => {
    function repeat(time){
      const position = index % noteValue.length
      const synthNote = noteValue[position]
      console.log(noteValue)
      synth.triggerAttackRelease(synthNote.note, time)
      index++
    }

    const unschedule = Tone.Transport.scheduleRepeat((time) => { 
      repeat(time)
    }, '4n')

    return unschedule;
  },[noteValue])

In short: repeat is moved into the useEffect, the useEffect gets a dependency on noteValue and returns a cleanup callback. Otherwise you would need a useCallback around repeat that would have noteValue as a dependency and add repeat as a dependency of the useEffect. This way it is both in one.

phry
  • 35,762
  • 5
  • 67
  • 81
  • Thanks for your help. I had a read of what you said but I'm still confused. I tried moving the variables within the function out of it but it didn't work. I also notice when I remove the second argument from ```useEffect``` then I can change the notes to play so I'm confused by that too. However, removal of this argument causes ```Tone.transport``` to run loads of times and lead to loads of notes being played. – JuniorWebDev28 Apr 10 '21 at 11:45
  • Changing the second argument to useEffect will cause it to re-run when one of the dependencies changes. But then you can also have a cleanup before an effect is re-run in which you would need to stop the old running effect. Reading recommendation (beyond the react docs of course): https://overreacted.io/a-complete-guide-to-useeffect/ – phry Apr 10 '21 at 12:45
  • Thank you Phry. I've read all of those materials plus the React docs and feel like I've tried absolutely everything but unfortunately, I just have no idea how to get it to work. – JuniorWebDev28 Apr 10 '21 at 18:11
  • I am adding an example solution above. – phry Apr 10 '21 at 21:00
  • Thank you so so much for your solution. I think it kind of makes sense (I'm sure it DOES make sense but I'm a beginner) but now the sequence repeats on top of the original when I change the note state. Do you know why this is? Could you suggest how I might be able to fix that? Thank you for your time once again – JuniorWebDev28 Apr 13 '21 at 21:13
  • Did you change your ` Tone.Transport.scheduleRepeat` to return a function that would make it stop, like I said? – phry Apr 13 '21 at 21:22
  • Yes, I believe so – JuniorWebDev28 Apr 13 '21 at 22:04
  • so calling `unschedule()` makes the music stop? – phry Apr 13 '21 at 22:25
  • No, unfortunately not. I copied your solution word for word. – JuniorWebDev28 Apr 14 '21 at 10:20
  • My solution did not include that (since I have no idea what that part of your app is doing), it just included the notion that you would have to write that functionality. You will need to update your `scheduleRepeat` method to return a function to stop that repeat in your own code. – phry Apr 14 '21 at 10:41