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 ???
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:
Evaluate the first argument, () => setCounter(counter => counter + 1), creating a function
Evaluate the second argument, [], creating an array
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 the various functions you're passing to useCallback or useMemo 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 instead 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.
Related
I have this code here:
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => {
console.log({ step }, "reff.current before incr ", reff.current);
reff.current = reff.current + step;
console.log("ref.curret after incr ", reff.current);
return c + step;
});
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => setStep((prevStep) =>(prevStep + 1))}
value="+ step"
/>
</>
);
}
You can view it here: https://codesandbox.io/s/gracious-roentgen-31ntb?file=/src/App.js:0-817
Note that I'm incrementing ref inside setCount callback
When the component loads a setInterval is started. If I trigger the useEffect again by incrementing step before 6 seconds pass, it clears the setInterval and creates a new one.
And now if I don't increment step again and wait for 6 seconds, I see step: 1 reff.current before incr, 0 for the first time the setInterval callback is called.
After incrementing in the next line(reff.current = reff.current + step) I see "ref.curret after incr ", 1
When the setInterval callback is called again after 6 seconds, I see
step: 1 reff.current before incr, 2 //how did this become 2
I don't understand how the value of reff.current is 2.
This only happens when I increment the step(which clears the first interval). If I set the initial step to 1 and don't increment it, I see expected values.. Just checked again. It doesn't work as expected.
I can't understand why the value of reff.current is 2 when the setInterval callback is called the second time.
Go to the sandbox link and
Click on the +step button once
Open the console and see
log 1:{step: 1} "reff.current before incr "0
log 2: ref.curret after incr 1
//second time callback is called
log 3: {step: 1} "reff.current before incr "2 //this should be 1
log 4: ref.curret after incr 3```
Your problem happens because you are performing a side effect (updating reff.current) inside the updater function passed to the setCount.
The beta react docs say that:
Updater functions run during rendering, so updater functions must be
pure and only return the result. Don’t try to set state from inside of
them or run other side effects. In Strict Mode, React will run each
updater function twice (but discard the second result) to help you
find mistakes.
You are running your app in StrictMode. To verify it comment it:
const rootElement = document.getElementById('root');
ReactDOM.render(
// <StrictMode>
<App />,
// </StrictMode>,
rootElement
);
and the problem doesn't happen anymore.
Can you please try out this code below,
Not sure about the exact reason why the value was incremented, but using it directly instead of returning from a function, did work as expected. Also it works for further step increment.
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
console.log({ step }, "reff.current before incr ", reff.current);
debugger;
// ------ modified here
let newVal = reff.current + step;
reff.current = newVal;
console.log("ref.curret after incr ", reff.current);
setCount(newVal);
// ------ modified here
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => {
e.preventDefault();
setStep((prevStep) => {
console.log(prevStep);
return Number(prevStep) + 1;
});
}}
value="+ step"
/>
</>
);
}
The image of the log
I have been experimenting with setInterval. Both the codes were executed in Chrome. Below is my react component
function App() {
let [value, setValue] = useState(0);
useEffect(() => {
const id = setInterval(() => console.log(value), 1000);
return () => clearInterval(id);
}, []);
return (
<div className="App">
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>+</button>
<button onClick={() => setValue(value - 1)}>-</button>
</div>
);
}
The console.log in setTimeout inside useEffect keeps printing 0 no matter how many times you increment or decrement the value via button click.
Following code is being executed in browser (Chrome) console
let value = 0;
setInterval(() => console.log(value), 1000);
value = 3; // can be considered as setValue (sort of)
Now the browser console prints 3 which is expected.
I wonder why there is a difference in behaviour and would appreciate if someone could point out the reason for this. Any other code snippet or link that demonstrate this even better would be great.
You need to pass value as a dependency to your useEffect. Unless you give the list of items that your effect dependent upon in the dependency array, it will keep using the initial value.
useEffect(() => {
const id = setInterval(() => console.log(value), 1000);
return () => clearInterval(id);
}, [value]);
İn your example useEffect only work once when page loaded. This is why you only get console.log(0). If you want to make it when value change you need specify that in useEffect
For example
UseEffect(() =>{ console.log(value) }, [value])
In your example when you click to button, first value sets after that useEffect works and setInternals function triggered after 1s.
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 ???
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:
Evaluate the first argument, () => setCounter(counter => counter + 1), creating a function
Evaluate the second argument, [], creating an array
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 the various functions you're passing to useCallback or useMemo 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 instead 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.
I am currently refactoring a react app from setState to hooks. I can't understand why the state variables aren't changed. Here is an example:
import React, { useState, useEffect } from 'react';
function Hook() {
const [num, setNum] = useState(1);
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, []);
const logNum = () => {
console.log(num);
}
const handleToggle = () => {
if (num == 1) {
console.log('setting num to 2');
setNum(2);
} else {
console.log('setting num to 1');
setNum(1);
}
}
return (
<div>
<button onClick={handleToggle}>TOGGLE BOOL</button>
</div>
);
}
export default Hook;
When i click the button, I was expecting the output to be something like:
// 1
// setting num to 2
// 2
// setting num to 1
// 1
But the output look like this:
Why is the updated num variable not logged?
Shouldn't the logNum() function always point to the current value of the state?
That's why effect dependencies have to be exhaustive. Don't lie about dependencies.
logNum closures over num, so on every rerender there is a new num variable containing the new value, and a new logNum function logging that value. Your effect however gets initialized only once, thus it only knows the first logNum. Therefore, you have to add logNum as a dependency, so that the effect gets updated whenever num and thus logNum changes:
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, [logNum]);
You'll notice that your effect does not correctly clean up, you should add a removeEventListener too.
return () => window.removeEventListener("mousemove", logNum);
Now if you debug this piece of code, you'll notice that the effect triggers on every rerender. That is because a new logNum function gets created on every rerender, no matter wether num changes or not. To prevent that, you can use useCallback to make the logNum reference stable:
const logNum = useCallback(() => console.log(num), [num]);
An alternative to all of this would be to use a reference to the current state:
const actualNum = useRef(num);
// that works no matter when and how this is executed
console.log(actualNum.current);
I would like to know the difference between the following two versions of code. Both versions do the same.
1) Here just the counter variable is used to get the current value
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(counter + 1)}>{counter}</button>;
}
2) This version passes a function to setCounter
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(c => c + 1)}>{counter}</button>;
}
The official documentation says:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So what's wrong with the first option? Are there some pitfalls?
With the particular code in your example, you have the previous value in hand, so there isn't much difference. But sometimes you don't. For example, suppose you wanted to have a memoized callback function. Due to the memoization, the value of counter gets locked in when the closure is created, and won't be up to date.
const Counter = () => {
const [counter, setCounter] = useState(0);
// The following function gets created just once, the first time Counter renders.
const onClick = useCallback(() => {
setCounter(c => c + 1); // This works as intended
//setCounter(counter + 1); // This would always set it to 1, never to 2 or more.
}, []); // You could of course add counter to this array, but then you don't get as much benefit from the memoization.
return <button onClick={onClick}>{counter}</button>;
}