Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't `useCallback` always return the same ref

I don't understand why useCallback always returns a new ref each time one of the deps is updated. It results in many re-render that React.memo() could have avoided.

What is, if any, the problem with this implementation of useCallback?

export function useCallback(callback) {

    const callbackRef = useRef();

    callbackRef.current = callback;

    return useState(() =>
        (...args) => callbackRef.current(...args)
    )[0];

}

Using this instead of the built-in implementation sure has a significant positive impact on performance.

Own conclusion:

There is no reason not to use an implementation using ref over the built's in as long as you are aware of the implications, namely, as pointed out by @Bergy, you can't store a callback for use later (after a setTimeout for example) and expect the callback to have the same effect as if you'd have called it synchronously.
In my opinion however this is the preferred behaviour so no downside 🥂.

Update:

There is a React RFC for introducing a builtin hook that does just that. It would be called useEvent

like image 431
Joseph Garrone Avatar asked Jan 25 '21 18:01

Joseph Garrone


People also ask

What is useCallback return policy?

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Does useCallback cause re-render?

The key takeaway here is that useCallback returns you a new version of your function only when its dependencies change, saving your child components from automatically re-rendering every time the parent renders.

Should I wrap every function in useCallback?

useCallback() "Every callback function should be memoized to prevent useless re-rendering of child components that use the callback function" is the reasoning of his teammates.

For what purpose is the useCallback () hook used?

One reason to use useCallback is to prevent a component from re-rendering unless its props have changed. In this example, you might think that the Todos component will not re-render unless the todos change: This is a similar example to the one in the React.memo section.


1 Answers

What is, if any, the problem with this implementation of useCallback?

I suspect it has unintended consequences when someone stores a reference to your callback for later, as it will change what it is doing:

const { Fragment, useCallback, useState } = React;

function App() {
  const [value, setValue] = useState("");
  const printer = useCallback(() => value, [value]);
  return <div>
    <input type="text" value={value} onChange={e => setValue(e.currentTarget.value)} />
    <Example printer={printer} />
  </div>
}

function Example({printer}) {
  const [printerHistory, setHistory] = useState([]);
  return <Fragment>
    <ul>{
      printerHistory.map(printer => <li>{printer()}</li>)
    }</ul>
    <button onClick={e => setHistory([...printerHistory, printer])}>Store</button>
  </Fragment>
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id="root"></div>

(Sure, in this simplified demo the printer callback is nothing but a useless closure over the value itself, but you can imagine a more complex case where one could select an individual history entry and would want to use a complicated on-demand computation in the callback)

With the native useCallback, the functions stored in the printerHistory would be distinct closures over distinct values, while with your implementation they would all be the same function that refers to the latest useCallback argument and only prints the current value on every call.

like image 51
Bergi Avatar answered Sep 25 '22 16:09

Bergi