Search code examples
reactjsreact-hooksusecallback

Why does useCallback with an empty dependency array not return the same function?


I'm trying to write a custom React hook that returns functions which maintain reference equality with every invocation of the hook. Meaning that exactly the same function is returned from the hook every single time, such that comparing them with === returns true.

I was under the impression that the useCallback hook was the way to accomplish that:

function useCorrectCallback() {
  const [count, setCount] = useState(0)
  let increment = () => setCount(c => c + 1)

  let memoized = useCallback(increment, [])
  return {
    count,
    increment: memoized
  }
}

So when this hook is called, I believe that the increment property of the return value should be the exact same function every time. It should be the value of the inline function the first time the hook was run, and not change on subsequent executions of the hook because I have specified no dependencies to useCallback with [].

But that's not what seems to happen! I've written a test project to demonstrate this problem. Here's the crux of that test:

function validateFunctionEquality(hookResult) {
  // Store the last result of running the hook
  let stored = { ...hookResult }

  // Force a re-render to run the hook again
  expect(hookResult.count).toEqual(0)
  act(hookResult.increment)
  expect(hookResult.count).toEqual(1)

  // Compare the previous results to the current results
  expect(hookResult.constant).toEqual(stored.constant)
  expect(hookResult.increment).toEqual(stored.increment)
}

Just to validate that my tests are accurate, I also wrote a hook which just uses a globally scoped object to maintain "memory" instead of trying to ask React to do it with useCallback. This hook passes the tests:

let memoryHack = {}

function useMemoryHack() {
  const [count, setCount] = useState(0)
  let increment = () => setCount(c => c + 1)

  memoryHack.increment = memoryHack.increment || increment
  return {
    count,
    increment: memoryHack.increment
  }
}

Am I using useCallback incorrectly? Why does useCorrectCallback return a different function in its increment property on subsequent executions?


Solution

  • I suspect is an issue with enzyme's shallow. Probably related to https://github.com/airbnb/enzyme/issues/2086

    By using mount, your test pass as it is:

      act(() => {
        componentMount = mount( // used mount instead of shallow
          <Component>
            {hookValues => {
              copyHookValues(hookResult, hookValues)
              return null
            }}
          </Component>
        )
      })