Explanations with some examples:
Function in component body
function App() {
const [count, setCount] = useState(0);
const lastRenderTime = new Date().toString();
function bodyFn() {
alert("bodyFn: " + lastRenderTime);
}
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={bodyFn} />
</>
);
}
Whenever the <App>
component re-renders (e.g. if the count
state is modified), a new bodyFn
is created.
Should <SomeChildComponent>
monitor its onClick
prop reference (typically in a dependency array), it will see that new creation everytime.
But the event callback behaviour is as expected: whenever bodyFn
is called, it is the "most recent creation" of that function, and in particular it correctly uses the latest value of lastRenderTime
(the same as already displayed).
Common usage of useCallback
function App() {
const lastRenderTime = new Date().toString();
const ucbFn = useCallback(
() => alert("ucbFn: " + lastRenderTime),
[lastRenderTime]
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={ucbFn} />
</>
);
}
Whenever the <App>
re-renders, the lastRenderTime
value is different. Hence the useCallback
dependency array kicks in, creating a new updated function for ucbFn
.
As in the previous case, <SomeChildComponent>
will see that change everytime. And the usage of useCallback
seems pointless!
But at least, the behaviour is also as expected: the function is always up-to-date, and shows the correct lastRenderTime
value.
Attempt to avoid useCallback
dependency
function App() {
const lastRenderTime = new Date().toString();
const ucbFnNoDeps = useCallback(
() => alert("ucbFnNoDeps: " + lastRenderTime),
[] // Attempt to avoid cb modification by emptying the dependency array
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={ucbFnNoDeps} />
</>
);
}
In a "naive" attempt to restore the advantage of useCallback
, one may be tempted to remove lastRenderTime
from its dependency array.
Now ucbFnNoDeps
is indeed always the same, and <SomeChildComponent>
will see no change.
But now, the behaviour is no longer as one could expect: ucbFnNoDeps
reads the value of lastRenderTime
that was in its scope when it was created, i.e. the first time <App>
was rendered!
With custom useEventCallback
hook
function App() {
const lastRenderTime = new Date().toString();
const uecbFn = useEventCallback(
() => alert("uecbFn: " + lastRenderTime),
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={uecbFn} />
</>
);
}
Whenever the <App>
re-renders, a new arrow function (argument of useEventCallback
custom hook) is created. But the hook internally just stores it in its useRef
current placeholder.
The hook returned function, in uecbFn
, never changes. So <SomeChildComponent>
sees no change.
But the initially expected behaviour is restored: when the callback is executed, it will look for the current placeholder content, which is the most recently created arrow function. Which therefore uses the most recent lastRenderTime
value!
Example of <SomeChildComponent>
An example of a component that depends on the reference of one of its callback props could be:
function SomeChildComponent({
onClick,
}: {
onClick: () => void;
}) {
const countRef = useRef(0);
useEffect(
() => { countRef.current += 1; },
[onClick] // Increment count when onClick reference changes
);
return (
<div>
onClick changed {countRef.current} time(s)
<button onClick={onClick}>click</button>
</div>
)
}
Demo on CodeSandbox: https://codesandbox.io/s/nice-dubinsky-qjp3gk