The intent of useCallback
is to be able to leverage props or state that are in the current scope and that may change on re-render. The dependency-array then tells React when you need a new version of the callback. If you are trying to memoize an expensive computation, you need to use useMemo
instead.
The example below demonstrates the differences between useCallback
and useMemo
and the consequences of not using them. In this example I am using React.memo
to prevent Child
from re-rendering unless its props or state change. This allows seeing the benefits of useCallback
. Now if Child
receives a new onClick
prop it will cause a re-render.
Child 1 is receiving a non-memoized onClick
callback, so whenever the parent component re-renders, Child 1 always receives a new onClick
function so it is forced to re-render.
Child 2 is using a memoized onClick
callback returned from useCallback
and Child 3 is using the equivalent via useMemo
to demonstrate the meaning of
useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs)
For Child 2 and 3 the callback still gets executed every time you click on Child 2 or 3, useCallback
just ensures that the same version of the onClick
function is passed when the dependencies haven't changed.
The following part of the display helps point out what is happening:
nonMemoizedCallback === memoizedCallback: false|true
Separately, I'm displaying somethingExpensiveBasedOnA
and a memoized version using useMemo
. For demonstration purposes I'm using an incorrect dependency array (I intentionally left out b
) so that you can see that the memoized version doesn't change when b
changes, but it does change when a
changes. The non-memoized version changes whenever a
or b
change.
import ReactDOM from "react-dom";
import React, {
useRef,
useMemo,
useEffect,
useState,
useCallback
} from "react";
const Child = React.memo(({ onClick, suffix }) => {
const numRendersRef = useRef(1);
useEffect(() => {
numRendersRef.current++;
});
return (
<div onClick={() => onClick(suffix)}>
Click Me to log a and {suffix} and change b. Number of Renders:{" "}
{numRendersRef.current}
</div>
);
});
function App(props) {
const [a, setA] = useState("aaa");
const [b, setB] = useState("bbb");
const computeSomethingExpensiveBasedOnA = () => {
console.log("recomputing expensive thing", a);
return a + b;
};
const somethingExpensiveBasedOnA = computeSomethingExpensiveBasedOnA();
const memoizedSomethingExpensiveBasedOnA = useMemo(
() => computeSomethingExpensiveBasedOnA(),
[a]
);
const nonMemoizedCallback = suffix => {
console.log(a + suffix);
setB(prev => prev + "b");
};
const memoizedCallback = useCallback(nonMemoizedCallback, [a]);
const memoizedCallbackUsingMemo = useMemo(() => nonMemoizedCallback, [a]);
return (
<div>
A: {a}
<br />
B: {b}
<br />
nonMemoizedCallback === memoizedCallback:{" "}
{String(nonMemoizedCallback === memoizedCallback)}
<br />
somethingExpensiveBasedOnA: {somethingExpensiveBasedOnA}
<br />
memoizedSomethingExpensiveBasedOnA: {memoizedSomethingExpensiveBasedOnA}
<br />
<br />
<div onClick={() => setA(a + "a")}>Click Me to change a</div>
<br />
<Child onClick={nonMemoizedCallback} suffix="1" />
<Child onClick={memoizedCallback} suffix="2" />
<Child onClick={memoizedCallbackUsingMemo} suffix="3" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here's a related answer: React Hooks useCallback causes child to re-render