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)