9

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

Joseph Garrone
  • 1,662
  • 19
  • 20
  • 2
    I'm guessing that the default behaviour is to keep re-renders when the callback function needs to change, but avoid re-rendering the whole tree each time a callback is inlined (new one created each render, regardless of deps). – Emile Bergeron Jan 25 '21 at 19:06
  • If you want the same ref, then you should used with no dependencies `useCallback(..., [])`. If it depends on some state, you must get the new values each time they change. That's why useCallback needs to use a new function. – BeS Jan 26 '21 at 09:14
  • 1
    good question. I suggest to remove the Typescript and eslint parts, as they are irrelevant to the question. – kca Jan 27 '21 at 08:27
  • @BeS Use this implementation of use callback instead of the default, you'll see, it works just as well. – Joseph Garrone Jan 28 '21 at 13:25
  • 1
    Interestingly, the [new React.js docs](https://beta.reactjs.org/apis/useref) advise "*Do not write or read `ref.current` during rendering.*" – Bergi May 09 '22 at 12:38

2 Answers2

5

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/react@16.14.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/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.


For a much longer elaboration, see the useEvent proposal. There are definitely use cases for this, like solving the stale closure problem, but that's a different problem from what useCallback solves.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    This is a very good example. Thank you for pointing it out. Now I think I understand why the react team didn't make my implementation the default one and I feel safer using it for myself. – Joseph Garrone Jan 28 '21 at 14:26
  • I won't mark it as answered just yet because someone told me that it might have some consequences with the upcoming react concurrent mode and I would like to know more about that. – Joseph Garrone Jan 28 '21 at 14:27
  • 1
    @JosephGarrone I'm a library author and have looked a lot into the concurrent mode features already available. Your `useMemo()` is unnecessary, you could just always assign the latest `callback` to `ref.current` and forego the `deps` parameter. Your implementation will behave the same in concurrent mode as it does in legacy mode. – Patrick Roberts Jan 28 '21 at 16:55
  • @PatrickRoberts you are absolutely right!!! I don't know how I could have missed that out! But if with `useMemo()` there is a potential problem with CM that's right? – Joseph Garrone Jan 28 '21 at 18:50
  • 1
    @JosephGarrone no, your `useMemo()` doesn't have any side-effects that would cause different behavior with multiple renders. If it works fine in [strict mode](https://reactjs.org/docs/strict-mode.html) (which it does), it will work fine in concurrent mode as well. – Patrick Roberts Jan 28 '21 at 20:23
  • Good example, indeed, but I still would like to see a real-world example, where the _useRef_- version would really not work. E.g. in this example I would simply pass `value` instead` of `printer`. -- I'm fine with the react version, but I really can't imagine a use case where I could not cleanly work with the _useRef_- version as well. – kca Jan 29 '21 at 20:48
  • 1
    @kca Having an object "method" in the props that computes a result is just one example, having a callback that executes a side effect would be another. As soon as the child component intends to keep multiple different versions of the passed value for later, say e.g. in the closure of a `useEffect` with a timeout or network request, not getting the original reference will cause different behaviour. – Bergi Jan 29 '21 at 20:58
0

The use cases of useCallback and useMemo are different. You said that useCallback returns a memoized version of the callback that only changes if one of the dependencies has changed. As a result, it rerenders the components according to the change of dependencies. But useMemo only hold mutable value. Generally, we use useCallback for processing event handler. For example, let's assume that when you click the button, the count of button click is displayed. In this case, click event is implemented using useCallback.

 const [count, setCount] = useState(0);
 const handleClick = useCallback(() => {
   setCount(count + 1);
   console.log(count + 1);
 }, [count]);

To get new increased count value and display its value whenever the button is clicked, it is needed to rerender. Say again, the use cases of useCallback and useMemo are depended on the purpose.

Jhonatan
  • 11
  • 4