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:
Button
is memoized. (In that case by React.memo
, but it could be a class component memoized via shouldComponentUpdate
.)
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)