I’m building a React application that asks the user for an input, and if correct (enter "3"), we render a 10×10 grid of buttons, each tied to a click handler.
When any button is clicked, it should print to the console the internal representation of the 2d grid array, ie , the state value of grid
.
Here is the basic code:
The Grid
component, which takes a 2d grid array, and creates a button array. Each cell
is { onClick: (...) }
const Grid = ({ grid }) =>
grid.flat().map((cell, index) => (
<button key={index} onClick={() => cell.onClick(index)}>
{index}
</button>
));
Here is the Input
component, which uses setInput
from parent
const Input = ({ setInput }) => {
const [_input, _setInput] = React.useState("");
return (
<div>
<input onChange={(e) => _setInput(e.target.value || "")} />
<button onClick={() => setInput(_input)}>Play</button>
</div>
);
};
Here is the main App
component. It renders the Input
, and based on a useEffect
hook, if the input is valid (its 3), it updates the valid
state, and also should create the grid
state.
const App = () => {
const [valid, setValid] = React.useState(false);
const [grid, setGrid] = React.useState([]);
const [input, setInput] = React.useState(null);
const reveal = () => {
console.log("grid is ", grid);
};
const createGrid = () => {
if (input !== "3") return [];
const arr = Array.from({ length: 10 }, () =>
Array.from({ length: 10 }, () => ({
onClick: () => reveal(),
}))
);
console.log("setting grid to ", [...arr]);
setGrid([...arr]);
};
React.useEffect(() => {
setValid(input == "3");
createGrid();
}, [input]);
return (
<div>
<Input setInput={setInput} />
{valid && <Grid grid={grid} />}
</div>
);
};
export default App;
I can see that when the component is initially rendered, and we enter "3", the grid state does get set, but when I click on a button, the reveal
function prints that the grid state is []
. Why is this happening?
2
Answers
This happens because of stale closure
The
reveal
that is stored in the state is not the same as thereveal
that is reinitialized on each render inside the body of the component.When you store it in the state, it contains the current value of the
grid
and it does not get updated when you change the value ofgrid
Usually the callback functions are not stored in a state if there is no reason to do that.
Be careful what you use for
key
, it can lead to unexpected behaviorLet us have a overview of the fundamental working principles applicable in this case:
About useEffect
About snapshot of states
About closures
instead it maintains an access to the scope. It means it sees the latest values of the variables in its scope.
Now we shall come to your case:
The flow of the control goes here as below:
The app is ready of input after its initial render and execution of useEffect.
Now we enter 3 in the input.
On clicking the button, the state input will be changed so.
This state change triggers a new render.
After the new render, useEffect will be followed.
Now we are in the event useEffect.
6.1 We have already stated that useEffect sees only a snapshot of the states. Therefore snapshot of the state grid in the lates render is [].
6.2. Next coming to the following code.
6.3 The above code essentially creates an array, and more specifically by initialising the array item with the following closure.
6.4 The above closure has access to the state grid since it is in its enclosing function App. However at the rime of creating the closure, the value of grid is [].
6.5 Though the following code changes the state grid, it gets executed only in the next render. Therefore the value of the state grid during the run of the event useEffect remains the same as []. This is the reason all click events triggered show only [].
6.6 To put an emphasis to the issue, all click events will show the correct result when the input 3 is entered for a second time. The reason for this is that, when the input 3 was entered for the first time, the state grid has been populated with the closure accessing the filled-in array. When the input 3 is entered for a second time, all newly created closures will be accessing the filled-array.
Solution
As state is a snapshot, please do not use it to refer in closures. Instead simply access the local variable in the closure. The following code will give the desired result.
or this code, if you still want to use the helper function as well.
App function modified – full listing