skip to Main Content

Javascript binds variables to functions as they are created, which can cause ‘dangling’ out-of-date values to be used in useEffect.

function Counter() {
  var [count, setCount] = useState(1);
  
  useEffect(() => {
    var timeout = setInterval(() => setCount(count => count+1), 1000); // every 1'

    setTimeout(() => console.log(`Count after 10': ${count}`), 10000); // after 10'

    return () => clearTimeout(timeout);
  }, []);


  return <span>{count}</span>
}

The anonymous function of setInterval binds to count when it’s 1, and therefore the console.log output isn’t 10 (but 1).

Is there an elegant fix? For example, adding count as a dependency of useEffect would re-create the timeout in this case, causing unwanted behavior – although it would be a fix in many similar use cases.

Can I pass count by reference somehow without the boilerplate of wrapping it in an object and needing to keep that object in sync?

2

Answers


  1. You can use a useRef hook to grab the current value of count when your timer runs out.

    function Counter() {
      var [count, setCount] = useState(1);
      var countRef = useRef(count);
      useEffect(() => { 
        countRef.current = count; 
      }, [count])  
      
      useEffect(() => {
        var timeout = setInterval(() => setCount(count => count+1), 1000); // every 1'
    
        setTimeout(() => console.log(`Count after 10': ${countRef.current}`), 10000); // after 10'
    
        return () => clearTimeout(timeout);
      }, []);
    
    
      return <span>{count}</span>
    }
    
    Login or Signup to reply.
  2. @Caleth included an approach with using just useRef in his original answer. As I pointed out, that will not trigger re-render. To fix that, a dummy state can be introduced. Then just one useEffect is required.

    import { useEffect, useState, useRef } from "react"
    
    function Counter() {
      const [dummy, setDummy] = useState(0) // Dummy state to force re-render
      const countRef = useRef(1)
    
      useEffect(() => {
        const interval = setInterval(() => {
          countRef.current += 1
          setDummy((dummy) => dummy + 1) // Trigger a re-render by updating dummy
        }, 1000)
    
        setTimeout(() => {
          console.log(`Count after 10 seconds: ${countRef.current}`)
        }, 10000)
    
        return () => clearInterval(interval)
      }, [])
    
      return <span>{countRef.current}</span>
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search