3

I have some react component, that looks like this:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

It seem to basicly work well. However, several videos and articles we found, insisted that this will result in a loop of re-renders. They recommend we do something like this:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);
  const incCounter = () => setCount(count + 1);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={incCounter}>
        Click me
      </button>
    </div>
  );
}

While on this example it's not that bad, longer forms create such a long list of functions that make the code ugly. As everything works well, should I fear the first part will result in useless renders? If not, can you explain what other reasons we have to move the functionality into a function inside the same object, when all it does it update a state?

GuruYaya
  • 545
  • 3
  • 16
  • Both are same. In the first one `anonymous` function is created and in the other you are creating a function outside of JSX but it is not anonymous. – DecPK Aug 15 '22 at 07:10
  • 3
    No, you don't have to do this. It will not result in a loop. The articles weren't good quality or you misunderstood the point. – Konrad Aug 15 '22 at 07:11
  • 1
    *this will result in a loop of re-renders*, It won't. Let's take a single scenario here where react will re-render component only if we change state. Here you are changing state only if a user click on the button. So in both case you are creating a callback function, which will trigger only if user click on the button. So there is no chance of *loop of re-renders* – DecPK Aug 15 '22 at 07:11

1 Answers1

0

However, several videos and articles we found, insisted that this will result in a loop of re-renders.

Either those videos and articles are incorrect, or you didn't quite understand the point they were making. Your two pieces of code are basically the same. But there would be a difference if you used useCallback along with the callback form of the state setter:

const incCounter = useCallback(
    () => setCount((c) => c + 1),
    []
);

That makes no difference to the number of re-renders of your component, but it can make a difference to the number of re-renders of the child components you're rendering.

In your specific example, that useCallback is unnecessary; the child elements you're creating are very simple and rerendering isn't a problem. But if you were passing incCounter to a complex child component and that child component was memoized, it would make a difference to how often that child component would be re-rendered, because using useCallback and the callback form of the state setter makes incCounter stable (the same function is used throughout the lifetime of your component instance),¹ which means that the props of the element you're passing it to don't change unnecessarily. That matters for a memoized component.

There's more detail about that in my answer to this other question. Here's the relevant example from that answer:

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. That's because:

  1. Button is memoized. (In that case by React.memo, but it could be a class component memoized via shouldComponentUpdate.)
  2. ComponentA provides a stable increment prop to Button, via useCallback (which is just a convenience wrapper around useMemo).

¹ I said "throughout the lifetime of your component instance," but the documentation doesn't quite guarantee that. From the useMemo docs linked above (and again, useCallback is just a wrapper around useMemo):

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

(their emphasis)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875