Dealing with async in React
Identifying and fixing temporal problems in React
The problem
Handling asynchronous operations in React is tricky sometimes. The framework handles synchronous operations quite well, but, unlike in AngularJs, there is no support for promises and other deferred executions. If you do not test and handle the corner cases, bugs are easily go unnoticed.
Temporal issues are the hardest to debug. If you don't know where to look, then a simple bug report might not be enough. Knowing these corner cases might come handy if you are working on a non-trivial React app.
This post demonstrates the basic error cases and potential solutions.
Basic example
Click here for a live demo.
And click here for a possible solution.
Let's consider the following use case: you have a long-running process, for example a fetch from a remote database, and you display the results in the page. If the user navigates to another page, the component will be unmounted and you'll have a warning at the console.
q.delay(2000).then(() => {
this.setState({result: "finished"});
}).done();
The solution is quite simple; just add an isMounted() guard before the setState(), and the warning is gone.
q.delay(2000).then(() => {
if (this.isMounted()) {
this.setState({result: "finished"});
}
}).done();
A more complicated example
Click here for a live demo.
The above solution is not enough if you initiate the long-running process on a props change. If the user changes the value of the prop, then the component will be still mounted. If you don't introduce any other checks, then both async processes will be effective, possibly resulting in corrupted state.
q.delay(2000).then(() => {
this.setState({result: `connected to ${selected}`});
}).done();
A slightly improved version
Click here for a live demo.
The simplest solution you would think of is to introduce a check to see if the prop value is the same when the process is finished as was when it started. This would mitigate the issue to some extent, but if the user rapidly changes the value back and forth, she would still see some flickering.
q.delay(2000).then(() => {
if (this.props.selected === selected) {
this.setState({result: `connected to ${selected}`});
}
}).done();
A real solution
Click here for a live demo.
Saving the timestamp to the component state and checking that when the deferred process finishes would solve the problem properly. It requires an extra value in the component state, but ensures that only the last process will be actually run.
const timestamp = new Date().getTime();
this.setState({timestamp});
q.delay(2000).then(() => {
if (this.state.timestamp === timestamp) {
this.setState({result: `connected to ${selected}`});
}
}).done();
A don't-try-this-at-home hack
Click here for a live demo.
If you add the prop to the component's key is a fragile but valid solution. This renders the isMounted() guard sufficient, as if the user changes the prop then React will remove and replace the component.
But this makes the component fragile, as the consistency must be guaranteed by the enclosing component. If you fail to add the key parameter somewhere, your component will cease to work.
<DatabaseConnection selected={this.state.selected} key={this.state.selected}/>
Summary
These are the basic cases where an error to deferred execution might get introduced. They all work well in a controlled environment; if you don't click around until the operation finishes, everything looks good. But real users will twiddle with your application when you would patiently wait. And they will notice and report the errors.