Search code examples
reactjsreact-hookssetintervaluse-effectclearinterval

ReactJS Use SetInterval inside UseEffect Causes State Loss


So I am writing a product prototype in create-react-app, and in my App.js, inside the app() function, I have:

const [showCanvas, setShowCanvas] = useState(true)

This state is controlled by a button with an onClick function; And then I have a function, inside it, the detectDots function should be ran in an interval:

const runFaceDots = async (key, dot) => {
const net = await facemesh.load(...);
setInterval(() => {
  detectDots(net, key, dot);
}, 10);
// return ()=>clearInterval(interval);};

And the detectDots function works like this:

  const detectDots = async (net, key, dot) => {
...
  console.log(showCanvas);
  requestFrame(()=>{drawDots(..., showCanvas)});
  }
}};

I have a useEffect like this:

useEffect(()=>{
runFaceDots(); return () => {clearInterval(runFaceDots)}}, [showCanvas])

And finally, I can change the state by clicking these two buttons:

 return (
     ...
      <Button 
        onClick={()=>{setShowCanvas(true)}}>
          Show Canvas
      </Button>
      <Button 
        onClick={()=> {setShowCanvas(false)}}>
          Hide Canvas
      </Button>
    ...
    </div>);

I checked a few posts online, saying that not clearing interval would cause state loss. In my case, I see some strange behaviour from useEffect: when I use onClick to setShowCanvas(false), the console shows that console.log(showCanvas) keeps switching from true to false back and forth.

a screenshot of the console message

you can see initially, the showCanvas state was true, which makes sense. But when I clicked the "hide canvas" button, and I only clicked it once, the showCanvas was set to false, and it should stay false, because I did not click the "show canvas" button.

I am very confused and hope someone could help.


Solution

  • Try using useCallback for runFaceDots function - https://reactjs.org/docs/hooks-reference.html#usecallback

    And ensure you return the setInterval variable to clear the timer.

    const runFaceDots = useCallback(async (key, dot) => {
         const net = await facemesh.load(...);
         const timer = setInterval(() => {
            detectDots(net, key, dot);
         }, 10);
         return timer //this is to be used for clearing the interval
     },[showCanvas])
    

    Then change useEffect to this - running the function only if showCanvas is true

    useEffect(()=>{
           if (showCanvas) {
           const timer = runFaceDots(); 
            return () => {clearInterval(timer)}
           }
           }, [showCanvas])
    

    Update: Using a global timer

    let timer // <-- create the variable outside the component.
    
    const MyComponent = () => {
         .....
        useEffect(()=>{
               if (showCanvas) {
               runFaceDots();  // You can remove const timer here
                return () => {clearInterval(timer)}
               } else {
                   clearInterval(timer) //<-- clear the interval when hiding
               }
                
               }, [showCanvas])
    
        const runFaceDots = useCallback(async (key, dot) => {
             const net = await facemesh.load(...);
             timer = setInterval(() => { //<--- remove const and use global variable
                detectDots(net, key, dot);
             }, 10);
             return timer //this is to be used for clearing the interval
         },[showCanvas])
    
         .....
    }