Nested asynchronous setState and setState inside Promises #3734
-
I've been upgrading my codebase from Preact 8 to 10. In the Preact X upgrading guide it says not to use state synchronously anymore, which is fine, but I want to be sure I'm not potentially falling into a race condition or something. I'll start with a simple example. I have a component acting as a button. The onClick() {
if (this.state.loved) {
$Love.Remove(this.props.node.id)
.then(() => {
this.setState({'loved': false});
});
}
else {
$Love.Add(this.props.node.id)
.then(() => {
this.setState('loved': true});
});
}
} I'm aware that if the code wasn't asynchronous, the solution would be something like this. onClick() {
this.setState(prevState => {
if (prevState.loved) {
Love.Remove(this.props.node.id);
return {'loved': false};
}
else {
Love.Add(this.props.node.id);
return {'loved': true};
}
});
} Unfortunately it is, so should I just wrap the original code in a throwaway onClick() {
this.setState(prevState => {
if (prevState.loved) {
$Love.Remove(this.props.node.id)
.then(() => {
this.setState({'loved': false});
});
}
else {
$Love.Add(this.props.node.id)
.then(() => {
this.setState('loved': true});
});
}
return null;
});
} Tangentially, I was running into issues with more creatively nested // Asynchronous
onClick1() {
this.setState(prevState => {
if (prevState.loved) {
$Love.Remove(this.props.node.id)
.then(() => {
this.setState({'loved': false});
});
}
else {
$Love.Add(this.props.node.id)
.then(() => {
this.setState('loved': true});
});
}
return {'clicks': prevState.clicks+1};
});
}
// Synchronous
onClick2() {
this.setState(prevState => {
// Yes I realize I could return {'loved': !prevState.loved, 'clicks': prevState.clicks+1}
this.setState({'loved': !prevState.loved});
return {'clicks': prevState.clicks+1};
});
} Am I at risk of corrupting anything if a Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
To be honest the code looks perfectly fine and I'd keep the same for Preact 10. The text in the upgrade guide refers to the fact that you cannot immediately read the new values from state = { foo: false }
// Preact 8.x
this.setState({ foo: true });
console.log(this.state.foo) // Logs: true
// Preact 10.x
this.setState({ foo: true });
console.log(this.state.foo) // Still logs previous value: false That's what that section is referring to. The The easiest way to avoid a race condition here is to disable the button for as long as your async code runs: onClick() {
// Button is disabled based on this state
this.setState({ disabled: true })
const loved = this.state.loved;
// We don't care which action we fire, just that it returns a promise where
// we need to attach a `.finally()` to reset disabled state.
const promise = loved ? $Love.Remove(this.props.node.id) : $Love.Add(this.props.node.id)
promise
.then(() => {
// Both actions seem to invert the "loved" flag in the original example
this.setState({ loved: !loved });
})
// Make sure to re-enable button again, regardless if the operation
// was successful or threw an error
.finally(() => this.setState({ disabled: false });
} If you want to get more fancy and actually abort the request, then I'd recommend looking into
I'm not even sure if we have a test for that. This would certainly fall under the category of "undefined behavior". |
Beta Was this translation helpful? Give feedback.
To be honest the code looks perfectly fine and I'd keep the same for Preact 10. The text in the upgrade guide refers to the fact that you cannot immediately read the new values from
this.state
. In both Preact 8.x and 10.x the update is applied asynchronously to the DOM.That's what that section is referring to.
The
setState
…