DEV Community

Matan Shaviro
Matan Shaviro

Posted on

Understanding useCallback and Create Your Own

The useCallback hook in React used to memorize functions. It ensures that a function remains the same (referentially equal) between renders, unless its dependencies change. This is particularly useful when passing function as props to child components to prevent unnecessary re-renders.

Here's an example to demonstrate how useCallback works:

import { useEffect, useState } from 'react'

function CounterChild({ getCount }: { getCount: () => number }) {
  const [childCount, setChildCount] = useState(0)

  useEffect(() => {
    setChildCount(getCount())
    console.log('CounterChild rendered')
  }, [getCount])

  return <h2>Child Count: {childCount}</h2>
}

export default function CounterParent() {
  const [parentCounter, setParentCounter] = useState(0)
  const [, setToggle] = useState(false)

  const getCount = () => parentCounter

  return (
    <div>
      <h1>Count: {parentCounter}</h1>
      <CounterChild getCount={getCount} />
      <button onClick={() => setParentCounter((prev) => prev + 1)}>
        Increment
      </button>
      <button onClick={() => setToggle((prev) => !prev)}>Toggle</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

What Happens During Rendering?

When the parentCounter changes, via increment button, the getCount function is recreated, and as a result, it triggers a re-run of the useEffect hooks in the CounterChild component, causing the child component to re-render.

Similarly, when you toggle the setToggle button, although the parent component re-renders, it causes the getCount function to be recreated again, triggering an unnecessary re-render of the CounterChild component.

How to Fix This Using useCallback

To prevent unnecessary re-renders of CounterChild when setToggle is clicked, we can wrap the getCount function in a useCallback hook. This will ensure that the function getCount only changes when parentCounter changes, not when other state variables like setToggle change.

import { useEffect, useState, useCallback } from 'react'

function CounterChild({ getCount }: { getCount: () => number }) {
  const [childCount, setChildCount] = useState(0)

  useEffect(() => {
    setChildCount(getCount())
    console.log('CounterChild rendered')
  }, [getCount])

  return <h2>Child Count: {childCount}</h2>
}

export default function CounterParent() {
  const [parentCounter, setParentCounter] = useState(0)
  const [, setToggle] = useState(false)

  const getCount = useCallback(() => parentCounter, [parentCounter])

  return (
    <div>
      <h1>Count: {parentCounter}</h1>
      <CounterChild getCount={getCount} />
      <button onClick={() => setParentCounter((prev) => prev + 1)}>
        Increment
      </button>
      <button onClick={() => setToggle((prev) => !prev)}>Toggle</button>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

After we understood how useCallback works,
Let's create our own useCallback

import React from 'react'

const useCustomCallback = <T>(
  callback: () => T,
  dependencies: unknown[]
): (() => T) => {
  const callbackRef = React.useRef<() => T>(callback)
  const dependenciesRef = React.useRef<unknown[]>(dependencies)

  const hasChanged =
    !dependenciesRef.current ||
    dependenciesRef.current.some((dep, index) => dep !== dependencies[index])

  if (hasChanged) {
    callbackRef.current = callback
    dependenciesRef.current = dependencies
  }

  return callbackRef.current
}

export default useCustomCallback

Enter fullscreen mode Exit fullscreen mode

Lets break it down:

const callbackRef = React.useRef<() => T>(callback)
Enter fullscreen mode Exit fullscreen mode
  • callbackRef is a useRef hook that holds a reference to the latest version of the callback function. useRef doesn't trigger a re-render when its value changes, which makes it perfect for storing mutable data that does not affect the UI directly. This allows the hook to "remember" the callback without triggering re-renders when the callback updated.
const dependenciesRef = React.useRef<unknown[]>(dependencies)
Enter fullscreen mode Exit fullscreen mode
  • dependenciesRef is another useRef hook that stores the dependencies array passed to the hook. This is used to check if the dependencies have changed between renders.
const hasChanged =
  !dependenciesRef.current ||
  dependenciesRef.current.some((dep, index) => dep !== dependencies[index])

Enter fullscreen mode Exit fullscreen mode
  • !dependenciesRef.current: This part checks if the dependenciesRef has been initialized or not.

dependenciesRef.current.some(...): This part checks if any of the elements in the dependenciesRef.current array are different from the corresponding elements in the new dependencies array.

If either of these conditions is true (dependencies are missing or one of the dependencies has changed), hasChanged becomes true.

if (hasChanged) {
  callbackRef.current = callback
  dependenciesRef.current = dependencies
}

Enter fullscreen mode Exit fullscreen mode
  • When hasChanged is true, it means the dependencies have changed, and we need to update the callbackRef and dependenciesRef to reflect the new callback function and the new dependencies.
return callbackRef.current
Enter fullscreen mode Exit fullscreen mode
  • Finally, the hook returns callbackRef.current, which is the memoized version of the callback function. This version will remain the same unless the dependencies array changes.

This logic ensures that your application only updates when necessary, improving performance especially when passing functions as props to child components.

Happy coding :)

Top comments (0)