Reputation: 2978
So I'm semi-new to hooks. I want to run some basic validation. Running into a strange issue: when I run two hooks back-to-back, only the second of two hooks works.
const [validationTracking, setValidationTracking] = useState({});
const setValidation = (idx, field, value) => {
const validationCopy = cloneDeep(validationTracking);
if (!validationCopy[idx]) {
validationCopy[idx] = {};
}
validationCopy[idx][field] = value;
setValidationTracking(validationCopy);
};
const validateInputs = () => {
partnerInfo.forEach((object, idx) => {
if (!object['title']) {
setValidation(idx, 'title', true);
}
if (!object['body']) {
setValidation(idx, 'body', true);
}
});
};
In the above code partnerInfo=[{title: '', body: ''}]
The validation only gets triggered for body when I run validateInputs
If the array has more than one item, only the very last field will get its validation set to true [{body: true}]
The input above SHOULD set validationTracking to [{title: true, body: true}]
but it seems to skip or override earlier items
I know this.setState() in class-based components is async. I'm wondering if something similar is happening here..?
Upvotes: 2
Views: 1105
Reputation: 7725
You need to understand that useState
hook works in a functional way. When you call it, it triggers a re-render of the component, passing the new state value to it. State values are immutable, they are not references to values that can change. This is why we say that a React function components acts as pure functions with respect to their props.
So when you call setValidationTracking(validationCopy)
twice during a single update, you send two state updates that are computed using the current state for this iteration.
I.e: when the second loop calls cloneDeep(validationTracking)
, validationTracking
has not changed because the re-render triggered by the first loop has not happened and the state value is immutable any way.
To fix the problem you can instead pass a state updater function:
setValidationTracking(currentValidationTracking => ({
...currentValidationTracking,
[idx]: {
...(currentValidationTracking[idx] || {}),
[field]: value
}
}));
Upvotes: 0
Reputation: 30360
There are a few things to be aware of with the useState
hook:
The second point is more relevant to your question, given the forEach
iteration in your code makes multiple calls to the setValiadation
setter of your state hook. Because these are made in a single render cycle, only the last call to setValiadation
will have an observable effect.
The usual way to address this is to gather all state changes into a single object, and apply those with a single call to your setter. You could take the following approach to achieve that:
const [validationTracking, setValidationTracking] = useState({});
// Revised function
const updateValidation = (object, idx, field, value) => {
const validationCopy = cloneDeep(object);
if (!validationCopy[idx]) {
validationCopy[idx] = {};
}
validationCopy[idx][field] = value;
return validationCopy
};
const validateInputs = () => {
// Call setter via a callback that transforms current state
// into a new state object for the component
setValidationTracking(state => {
// Reduce partnerInfo array to a new state object
return partnerInfo.reduce((acc, infoObject, idx) => {
if (!infoObject['title']) {
acc = updateValidation(acc, idx, 'title', true);
}
if (!infoObject['body']) {
acc = updateValidation(acc, idx, 'body', true);
}
return acc;
}, state);
});
};
Hope that helps!
Upvotes: 1