I'm working on React project, and I was investigating some libraries. and I found they used 'useCallback" differently from what I've used. Below is that code part. I still think this code has no difference from using "useCallback" in a direct way though
// Saves incoming handler to the ref in order to avoid "useCallback hell"
export function useEventCallback<T, K>(handler?: (value: T, event: K) => void): (value: T, event: K) => void {
const callbackRef = useRef(handler);
useEffect(() => {
callbackRef.current = handler;
});
return useCallback((value: T, event: K) => callbackRef.current && callbackRef.current(value, event), []);
}
so my question is that what does mean by 'useCallback hell'? and what is the advantage of using "useCallback" such that way?
// BTW: I found a similar example on react documentation. but I still couldn't understand https://en.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
When you do a normal useCallback
, you must pass in a dependency array containing the variables your function uses. When one of them changes, the memoization breaks. This is fine in many cases, but sometimes your memoization is breaking all the time (because you depend on values that change all the time). When this happens, useCallback
provides little or no benefit.
The code you've shown has the goal that the memoization never breaks, even if you have complicated dependencies. Note that when it calls useCallback
, it passes in an empty dependency array []
. That's combined with using a ref to be able to keep track of what the latest handler
is. Then when the function is eventually called, it will check the ref for the latest handler
and call it. That latest handler
has the latest values in its closure, so it behaves as expected.
This code does achieve its goal of never breaking memoization. However, it needs to be used carefully. If you are using react's concurrent rendering, and you call the function returned by useEventCallback
during rendering, you can get some unexpected results. It's only safe to call the function outside of rendering, such as in an event callback, which is why they named it useEventCallback
.
Explanations with some examples:
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 (eg 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).
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.
useCallback
dependencyfunction 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 , ie the first time <App>
was rendered!
useEventCallback
hookfunction 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!
<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
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.