Give the codes below
import { useState } from "react";
import "./styles.css";
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const inc1 = () => {
console.log("debug inc:", count1);
setCount1((prev) => prev 1);
};
const inc2 = () => {
console.log("debug inc2:", count2);
setCount2(count2 1);
};
const [processInc1] = useState(() => {
console.log("debug longProcessBeforeInc:", count1);
// Run some long process here
return inc1;
});
const [processInc2] = useState(() => {
console.log("debug longProcessBeforeInc:", count2);
// Run some long process here
return inc2;
});
console.log("debug render:", count1, count2);
return (
<div className="App">
<h3>
{count1} - {count2}
</h3>
<button onClick={inc1}>Inc 1</button>
<br />
<br />
<button onClick={inc2}>Inc 2</button>
<br />
<br />
<button onClick={processInc1}>Long Inc 1</button>
<br />
<br />
<button onClick={processInc2}>Long Inc 2</button>
</div>
);
}
inc1
, inc2
, processInc1
all works as expected where you increase value by 1 and it renders correctly.
So with inc1
, inc2
the main difference is setCount1((prev) => prev 1);
and setCount2(count2 1);
, and processInc1
, processInc2
basically return inc1
and inc2
respectively via useState first time the component renders.
I understand from here https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function that it has something to do with closure but given the example above I fail to wrap my head around why inc2
, and processInc1
works but not processInc2
?
Here is the link to the codesandbox
for the above
https://codesandbox.io/s/eloquent-morse-37xyoy?file=/src/App.js
CodePudding user response:
You are on the right path. As you were saying, this problem is actually caused by the use of the closure. So let's start by showing you a nice definition of closure:
A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.
Docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
This means that when you return the closure inc2 in the useState, you are also creating a copy of the variables that are used at that particular time (i.e. initial rendering) by that lexical environment (this includes the count2 value). This is why processInc2 will keep having the same old count2 value.
At the same time processInc1 function will work correctly because, by using a callback in the useState, you are always getting the current value of the count1 state.
Lastly, inc2 works because you are directly calling it when you click the button, so the count2 value gets evaluated at that moment that you call it (therefore it will most probably have the current value).
CodePudding user response:
A very important detail here is that inc1
and inc2
are re-defined each time the App
component renders.
function App() {
// ...
const inc1 = () => {
console.log("debug inc:", count1);
setCount1((prev) => prev 1);
};
// ...
}
You then store those 2 functions inside a state, that does never change.
const [processInc1] = useState(() => {
console.log("debug longProcessBeforeInc:", count1);
// Run some long process here
return inc1;
});
This will result in processInc1
and processInc2
that point to the very first definition of inc1
and inc2
(created on the first render).
The reason count1
and count2
never update in this first version of the function is because the variable(s) are never re-assigned. This is by design.
The only reason count1
and count2
change in a future render is because useState()
will return the new value. After you receive this new value inc1
and inc2
are re-defined.
processInc1
and processInc2
are then pulled out of a React state that holds the first definition of inc1
and inc2
, so usage of count1
and count2
inside those functions will refer to the first value of count1
and count2
.
When you do setCount2(count2 1)
inside inc2
and call it via processInc2
the value of count2
is still 0
and will never change. This is because processInc2
refers to the very first definition of inc2
, and not the current definition.
setCount1((prev) => prev 1)
works due to the different function signature. Where inc2
passes a static value (0 1
) to the setter, inc1
passes a transformation (as a callback). When you pass a function to setCount1
React will call that function with the current state as the sole argument. The return value is then used as the new state. So although processInc1
still uses the very first definition of inc1
. It will always be relevant, since it describes the transformation that must be made rather than the value that must be set.
Do note that console.log("debug inc:", count1);
will keep logging 0
when called via processInc1
for the above reasons.