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 auseRef
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 anotheruseRef
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 thedependenciesRef
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 thecallbackRef
anddependenciesRef
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 :)
Top comments (0)