简体   繁体   中英

why use 'ref' when you use useCallback instead of using useCallback directly

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 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 (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).

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 , ie 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

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM