Im begginer in React programming, and need some help. I got simple component, that uses useState and useEffect hooks. This components is getting a number (as props), how much checkboxes should it render. It looks like this .
Why does handleCheckbox method returns an empty array? Thanks for any ideas
okay, I spotted quite a few issues which are interlinked (thats okay, you're a beginner) so I am going to try untangle this with words.. the code is easy to untangle.
the crux of the issue? Stale closures and async nature of setState . So many people fall victim to this, and I have answered this question quite a few times in different ways. so don't stress.
React relies on rendering to ensure the latest state is contained with a functions closure
. Think about a closure
as a "snapshot" of variables at a specific point in time (or better yet, for a specific render cycle).
the useEffect in Component
has an empty dependency array. This means, in short, that it will only ever call the code within the useEffect once, when the component first mounts.
useEffect(() => {
//this code will run only once when the component mounts for the first time
//index has no bearing since index is a value type which never changes in your example
//useEffects always react to a change in value of a value type (number, boolean, etc) or if a reference changes of a reference type (spreading an object creates a new reference, for example)
},[])
Now back to stale closures: What the above useEffect does in your example, is create a "snapshot" of the values used in the handleCheckbockChange
function, as they are on the first render when passed into the callback methods for onChange
see below here
const handleCheckbockChange = (e, value) => {
const { checked } = e.target;
const states = [...buttonsState];
const indexOfCheckBox = states.findIndex((el) => el.value === value);
if (indexOfCheckBox !== -1) {
states[indexOfCheckBox].visible = checked;
}
setButtonsState(states);
console.log(states);
};
now, remember what I said about "closures" being snapshots? when buttonState is created, it was initialized to an empty array
const [buttonsState, setButtonsState] = useState([]);
so on the first render, your function actually looks something like this in memory
const handleCheckbockChange = (e, value) => {
const { checked } = e.target;
const states = [...[]]; //buttonStates is an emptyArray when this was created!
const indexOfCheckBox = [].findIndex((el) => el.value === value);
if (indexOfCheckBox !== -1) {
[][indexOfCheckBox].visible = checked;
}
setButtonsState([]);
console.log([]); //<-- EMPTY!!
};
but you may now wonder how on earth this is possible if you are calling setButtonsState
in the useEffect?
Well.. now we come to the async nature of setState.
setState batches the updates and only applies them on the next render, hence asyncronous.
Your states
variable is initalised in the useEffect, but only after a closure around handleCheckbockChange
was already created with your initial value of []
. Therefore, handleCheckbockChange never gets this value since by that point the closure assigned to your onChange callback was already created with the initial value.
all your click actions therefore call the very first snapshot of handleCheckbockChange
which ever gets the updated value of the buttonsState
, because the useEffect runs only once.
Solution
Check out this sandbox.. here again for reference
There are actually a few things you can do here, but the easiest way to solve your situation is to remove the creation of your components out of the useEffect, so they freely rerender getting the latest closure snapshot of handleCheckbockChange
in their onChange handlers, everytime a change to buttonsState
occurs.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.