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)
-
callbackRefis auseRefhook that holds a reference to the latest version of the callback function.useRefdoesn'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)
-
dependenciesRefis anotheruseRefhook 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 thedependenciesRefhas 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
hasChangedis true, it means the dependencies have changed, and we need to update thecallbackRefanddependenciesRefto 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 :)
Top comments (0)