Search code examples
javascriptreact-hooksclosures

How does React's useCallback read the variable in closures


In the following example, when the button is clicked, cb2 uses the memoized function. However, why doesn’t cb2 use the first render closures (countVal: 0)?

function TestHook() {

  const [count, setCount] = useState(0)
  let countVal = 0

  const cb = useCallback(() => {
    console.log('cb dep [count]', count, countVal)
  }, [count])

  const cb2 = useCallback(() => {
    console.log('cb2 dep []', count, countVal)
  }, [])

  useEffect(() => {
    cb() // first time: 0, 0 second time: 1, 0 
    cb2() // first time: 0, 0 second time: 0, 1
  })

  const add = () => {
    console.log('add', count)
    countVal ++
    setCount(count + 1)
  }

  return (
    <>
      <button onClick={add}>add</button>
    </>
  );
}

Question:
Can anyone explain the result of cb2() after a re-render?


Solution

  • First render:

    • count = 0
    • countVal = 0

    Second render (after clicking the button):

    • setCount(count + 1) triggers a state update that causes a re-render.
    • countVal is incremented manually (but it's not part of React state, so React does not track it for re-renders).
    • cb2() logs 0, 1, where:
      • count is still 0 because cb2 is memoized with an empty dependency array, meaning it does not capture the updated count (but it's logged as 0 in cb2).
      • countVal is 1 because countVal was incremented during the previous render. Even though React doesn’t track countVal, it still holds the updated value within the closure of cb2 from the second render.

    Why does cb2() log 0, 1 on the second render?

    useCallback([]): Since cb2 has no dependencies ([]), it doesn't recreate the callback after the first render. This means that on the second render, cb2 is still using the closure from the first render, but React captures the updated value of countVal due to how the JavaScript closure works.