Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should we use useCallback in every function handler in React Functional Components

let's say we have the components like this

const Example = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); 
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

When I passed the onClick handler as an arrow function, my eslint throw a warning:

error    JSX props should not use arrow functions        react/jsx-no-bind

As I read from an answer from this post: https://stackoverflow.com/questions/36677733/why-shouldnt-jsx-props-use-arrow-functions-or-bind#:~:text=Why%20you%20shouldn't%20use,previous%20function%20is%20garbage%20collected.

The short answer is because arrow function is recreated every time, which will hurt the performance. One solution proposed from this post is to wrapped in a useCallback hook, with empty array. And when I change to this, the eslint warning really disappear.

const Example = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

However, there is also another opinion saying that overusing useCallback will eventually slowdown the performance due to the overheads of useCallback. One example is here: https://kentcdodds.com/blog/usememo-and-usecallback

This is making me really confused? So for Functional Components, when dealing with inline function handler, should I just write the arrow function (ignore the eslint) or always wrap it in a useCallback ???

like image 981
Jake Lam Avatar asked Sep 30 '20 09:09

Jake Lam


People also ask

Should you use useCallback for every function?

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. This reasoning is far from the truth. Such usage of useCallback() without profiling makes the component slower.

When should useCallback be used?

The useCallback hook is used when you have a component in which the child is rerendering again and again without need. Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.

When should we not use useCallback and useMemo?

UseCallback is used to optimize the rendering behavior of your React function components, while useMemo is used to memoize expensive functions to avoid having to call them on every render. As a standard construction of hooks, those two solutions are not so different.

Why useCallback is used in React?

The React useCallback Hook returns a memoized callback function. Think of memoization as caching a value so that it does not need to be recalculated. This allows us to isolate resource intensive functions so that they will not automatically run on every render.


1 Answers

The short answer is because arrow function is recreated every time, which will hurt the performance.

This is a common misconception. The arrow function is recreated every time either way (although with useCallback subsequent ones may be thrown away immediately). What useCallback does is make it possible for the child component you use the callback on to not re-render if it's memoized.

Let's look at the misconception first. Consider the useCallback call:

const increment = useCallback(() => setCounter(counter => counter + 1), []);

That's executed like this:

  1. Evaluate the first argument, () => setCounter(counter => counter + 1), creating a function

  2. Evaluate the second argument, [], creating an array

  3. Call useCallback with those two arguments, get back a function

Compare with what you have if you don't use useCallback:

const increment = () => setCounter(counter => counter + 1);

That's much simpler: Create the function. It doesn't then have to do #2 and #3 above.

Let's move on to what useCallback actually does that's useful. Let's look at where the callback is used:

<Button onClick={increment} />

Now, suppose Button is memoized with React.memo or similar. If increment changes every time your component renders, then Button has to re-render every time your component changes; it can't be reused between renders. But if increment is stable between renders (because you used useCallback with an empty array), the memoized result of calling Button can be reused, it doesn't have to be called again.

Here's an example:

const { useState, useCallback } = React;

const Button = React.memo(function Button({onClick, children}) {
    console.log("Button called");
    return <button onClick={onClick}>{children}</button>;
});

function ComponentA() {
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

function ComponentB() {
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const increment = useCallback(
        () => setCount(count => count + 1),
        []
    );
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Note that clicking the button in ComponentA always calls Button again, but clicking the button in ComponentB doesn't.

When do you want to do that? That's largely up to you, but it probably makes sense when your component's state will change frequently in ways that don't affect the contents of increment and thus don't affect Button and if Button has to do significant work when rendered. Button probably doesn't, but other child components may.

For instance, the useCallback in my previous example is probably pointless if you use count as the text of the button, since that means Button has to re-render regardless:

const { useState, useCallback } = React;

const Button = React.memo(function Button({onClick, children}) {
    console.log("Button called");
    return <button onClick={onClick}>{children}</button>;
});

function ComponentA() {
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            <Button onClick={increment}>{count}</Button>
        </div>
    );
}

function ComponentB() {
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const increment = useCallback(
        () => setCount(count => count + 1),
        []
    );
    return (
        <div>
            <Button onClick={increment}>{count}</Button>
        </div>
    );
}

ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Also note that useCallback isn't free, it impacts the code in the callback. Look at the code in the callbacks in ComponentA and ComponentB in the examples. ComponentA (which doesn't use useCallback) can use the value of count that it closes over (within limits!), () => setCount(count + 1). But the one in ComponentB always has to use the callback form of the setter, () => setCount(count => count + 1). That's because if you keep using the first increment you create, the count it closes over will be stale — you'd see the count go to 1, but never further.


A final note: If you're re-rendering a component so often that creating and throwing away its various functions may be causing too much memory churn (a rare situation), you can avoid it by using a ref. Let's look at updating ComponentB to using a ref intead of useCallback:

const incrementRef = useRef(null);
if (!incrementRef.current /* || yourDependenciesForItChange*/) {
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    incrementRef.current = () => setCount(count => count + 1);
}
const increment = incrementRef.current;

That only creates the increment function once (in that example, since we don't have any dependencies), it doesn't create and throw away functions like using useCallback does. It works because the initial value of the ref is null, and then the first time the component function is called, we see that it's null, create the function, and put it on the ref. So increment is only created once.

That example does recreate the function we pass setCount every time increment is called. It's possible to avoid that, too:

const incrementRef = useRef(null);
if (!incrementRef.current) {
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const incrementCallback = count => count + 1;
    incrementRef.current = () => setCount(incrementCallback);
}
const increment = incrementRef.current;

const { useState, useRef } = React;

const Button = React.memo(function Button({onClick, children}) {
    console.log("Button called");
    return <button onClick={onClick}>{children}</button>;
});

function ComponentA() {
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

function ComponentB() {
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    const incrementRef = useRef(null);
    if (!incrementRef.current) {
        // Note: Can't use `count` in `increment`, need the callback form because
        // the `count` the first `increment` closes over *will* be slate after
        // the next render
        const incrementCallback = count => count + 1;
        incrementRef.current = () => setCount(incrementCallback);
    }
    const increment = incrementRef.current;
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

That's really going to 11 in terms of avoiding unnecessary function creation. :-)

It's a rare component that needs even that first level of optimization, much less the second level; but when/if you do, that's how you do it.

like image 189
T.J. Crowder Avatar answered Sep 23 '22 02:09

T.J. Crowder