Logo

dev-resources.site

for different kinds of informations.

Understanding useCallback and Create Your Own

Published at
12/1/2024
Categories
react
frontend
frontendchallenge
Author
Matan Shaviro
Categories
3 categories in total
react
open
frontend
open
frontendchallenge
open
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>
  )
}

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>
  )
}

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

Lets break it down:

const callbackRef = React.useRef<() => T>(callback)
  • 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)
  • 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])

  • !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
}

  • 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
  • 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 :)

Featured ones: