0

I have seen many tutorials on creating timers in React by doing something like this

  useEffect(() => {
     let interval = null;
       if (timeractive) {
         interval = setInterval(() => {
           setCount(count => count+ 1);
         }, 50);
       } else {
         clearInterval(interval);
       }
       return () => clearInterval(interval);
     },[timeractive,count]);

This is what I think is happening:

1 timeractive = true

2 useEffect gets called, declaring assigning the variable interval to a setInterval

3 setCount gets called, incrementing count.

4 useEffect gets called again since count is one of its dependencies. But the cleanup function from the previous useEffect runs first, clearing the variable interval

5 go back to step 2

I don't want to do things this way because it seems quite unintuitive. It is essentially creating a large "interval" by creating and clearing a bunch of smaller intervals that only lasts one iteration.

So I wrote the following code, trying to create a timer that does not depend on what's being incremented. That why I decided to use useRef so that the interval will be persist between renders.

This is a timer that increases the radius every 100ms and draw a circle with that radius (modulo 50), it is supposed to be an animation but it doesn't work. However the radius seems to be updating fine. So there is something going on with the drawcircle function

I asked a similar question here, that's why I decided to pass down an updater function in setRadius

Can someone explain the concept of closure in this case?

const {useState,useEffect,useRef} = React;

function Timer({active}) {
    const intervalRef = useRef(null);
    const canvasRef = useRef(null);
    const [width,height] = [500,500];
    const [radius, setRadius] = useState(30);
    useEffect(()=>{
        if(active){
            intervalRef.current = setInterval(()=>{
                drawcircle();
                setRadius(radius => radius + 1);
            },100)
        } else {
            clearInterval(intervalRef.current) 
        }
    },[active])
    const drawcircle = ()=>{
      const context = canvasRef.current.getContext('2d');
      context.clearRect(0,0,width,height)
      context.beginPath()
      context.arc(width/2,height/2,radius%50,0,2*Math.PI)
      context.stroke();
    }
    return (
    <div>
    <canvas ref={canvasRef} width={width} height={height}/>
    <p>radius is {radius}</p>
    </div>
    )
}

function Main() {
    const [active, setActive] = useState(false)
    return (
        <div>
            <Timer active={active}/>
            <button onClick={()=>{setActive(!active)}}>Toggle</button>
        </div>
    )
}


ReactDOM.render(<Main />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<div id="app"></div>

1 Answer 1

1

The interval callback use the drawcircle method of the first render on each iteration and that drawcircle method refers to the initial radius value. To solve this, use a ref to drawcircle method

drawcircleRef = useRef()
useEffect(()=>{
        if(active){
            drawcircleRef.current = drawcircle
            const interval= setInterval(()=>{
                drawcircleRef.current();
                setRadius(radius => radius + 1);
            },100)
           return ()=>  clearInterval(interval) 
        }
    },[active])
useEffect(()=>{
        drawcircleRef.current = drawcircle
    },[radius])
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.