DEV Community

Yar
Yar

Posted on

Need assistance with useEffect() dependencies

Hey there! So I'm trying to figure out how to use canvas element in React.

raf-screenshot

Here is how my playground looks like. Canvas element, a dot travelling around the board and a button to start / stop the animation

The Issue

The button is giving me hard time pausing and resuming the animation. When the dot stops programmatically, it takes a couple of extra clicks on the Start button to keep it moving.

I suspect it has to do with useEffect and its dependencies.

Do you think you could take a look and give me some advice?

The Code

I use requestAnimationFrame() method to update the animation.

const reqRef = useRef()
const previousTimeRef = useRef()

const animate = time => {

    // some animation

    if (previousTimeRef.current !== undefined) {
        const deltaTime = time - previousTimeRef.current
    }

    previousTimeRef.current = time
    reqRef.current = requestAnimationFrame(animate)
    // stop
    if (shouldStop) cancelAnimationFrame(reqRef.current)
}

useEffect(() => {
    // start the loop
    reqRef.current = requestAnimationFrame(animate)
    // clean up
    return () => cancelAnimationFrame(reqRef.current) 

}, [shouldStop, previousTimeRef.current])
Enter fullscreen mode Exit fullscreen mode
  • animate() function loops itself
  • useEffect() starts the animation
  • requestAnimationFrame() method generates new reqRef value with each run
  • in order to stop the animation you have to use cancelAnimationFrame(reqRef.current) with the current reqRef

Approach

I use shouldStop as a key to pause the animation.

 const [shouldStop, setShouldStop] = useState(true)

<button onClick={() => setShouldStop(!shouldStop)}>
Enter fullscreen mode Exit fullscreen mode

At the start it works as expected

  • The button flips the key
  • useEffect fires, as shouldStop is set as its dependency, and sets the loop
    if (positionX < 0) {
        setPositionX(290)
        setPositionY(165)
        setShouldStop(!shouldStop)
    }
Enter fullscreen mode Exit fullscreen mode

When the dot bounces at the edge, the app resets its position and flips the key back to true. The dot rests in the middle of the screen.

And now when I press the button, the key switches to false yet nothing happens. After the second click key switches to true again. And only on the third time the key switches to false and the dot starts moving.

So

I guess I have three questions 😼

  • Is it a proper approach overall?
  • What am I missing about the useEffect()?
  • How do you trace / investigate those issues?

Discussion (3)

Collapse
sargalias profile image
Spyros Argalias • Edited

Unfortunately I found it a bit hard to tell what's wrong because I can't see the whole thing to run it and debug it. It seems like it should be working.

Nevertheless, consider trying something like this to keep the code more clear and maybe fix the issues (depending on what the rest of the code is dong):

const reqRef = useRef()
const previousTimeRef = useRef()

const animate = time => {
    // some animation

    previousTimeRef.current = time
    reqRef.current = requestAnimationFrame(animate)
}

useEffect(() => {
    if (!shouldStop) {
        reqRef.current = requestAnimationFrame(animate)
    }
    return () => cancelAnimationFrame(reqRef.current)

}, [shouldStop])
Enter fullscreen mode Exit fullscreen mode

Changes made:

  • Don't have a cancelAnimationFrame call in animate if animate doesn't decide when the animation will finish. It looks like only the value of shouldStop decides whether the animation should run or not, which is not changed inside animate.
  • In useEffect, don't start a new animation if it shouldn't be running.

Another suggestion, consider changing shouldStop (a negative condition) to isAnimating (a positive condition) and invert the conditionals. Working with double negative conditions like if (!shouldStop) is more difficult than working with positive conditions if (isRunning).

Good luck :)

Collapse
ptifur profile image
Yar Author • Edited

Hey Spyros, thanks for suggestions!

I guess I should clarify that in fact I took the requestAnimationFrame() part from someone's example and didn't really change it as long as it's working. Just provided this condition to link the animation to the Stop button.

The way I understand it, requestAnimationFrame() does not work as a one line function, it has this intricate way of operating. It works only withing this looping function, generates the reference id with each run. And — I discovered it just now after more research — generates the timestamp each time and passes it back as an argument (time in my example).

You didn't ask 😜, but if you're interested, here's a great reference I found css-tricks.com/using-requestanimat...

Collapse
jordyvandomselaar profile image
jordyvandomselaar • Edited

Hi @ptifur ,

Do you still need help with this? If so, would you mind creating a minimal reproducible example on something like CodeSandbox? That would make it easier for us to see what's going on :)

What I can say is that whenever you need to set state based on a previous value, like when you're flipping shouldStop, instead of doing setShouldStop(!shouldStop), try setShouldStop(prevShouldStop => !prevShouldStop). This ensures that you're always using the latest shouldStop to update it instead of accidentally using a previous value: reactjs.org/docs/hooks-reference.h...