Is using async componentDidMount() good?
Is using componentDidMount()
as an async function good practice in React Native or should I avoid it?
I need to get some info from AsyncStorage
when the component mounts, but the only way I know to make that possible is to make the componentDidMount()
function async.
async componentDidMount() {
let auth = await this.getAuth();
if (auth)
this.checkAuth(auth);
}
Is there any problem with that and are there any other solutions to this problem?
Let's start by pointing out the differences and determining how it could cause troubles.
Here is the code of async and "sync" componentDidMount()
life-cycle method:
// This is typescript code
componentDidMount(): void { /* do something */ }
async componentDidMount(): Promise<void> {
/* do something */
/* You can use "await" here */
}
By looking at the code, I can point out the following differences:
- The
async
keywords: In typescript, this is merely a code marker. It does 2 things:- Force the return type to be
Promise<void>
instead ofvoid
. If you explicitly specify the return type to be non-promise (ex: void), typescript will spit an error at you. - Allow you to use
await
keywords inside the method.
- Force the return type to be
- The return type is changed from
void
toPromise<void>
- It means you can now do this:
async someMethod(): Promise<void> { await componentDidMount(); }
- It means you can now do this:
-
You can now use
await
keyword inside the method and temporarily pause its execution. Like this:async componentDidMount(): Promise<void> { const users = await axios.get<string>("http://localhost:9001/users"); const questions = await axios.get<string>("http://localhost:9001/questions"); // Sleep for 10 seconds await new Promise(resolve => { setTimeout(resolve, 10000); }); // This line of code will be executed after 10+ seconds this.setState({users, questions}); return Promise.resolve(); }
Now, how could they cause troubles?
- The
async
keyword is absolutely harmless. -
I cannot imagine any situation in which you need to make a call to the
componentDidMount()
method so the return typePromise<void>
is harmless too.Calling to a method having return type of
Promise<void>
withoutawait
keyword will make no difference from calling one having return type ofvoid
. -
Since there is no life-cycle methods after
componentDidMount()
delaying its execution seems pretty safe. But there is a gotcha.Let's say, the above
this.setState({users, questions});
would be executed after 10 seconds. In the middle of the delaying time, another ...this.setState({users: newerUsers, questions: newerQuestions});
... were successfully executed and the DOM were updated. The result were visible to users. The clock continued ticking and 10 seconds elapsed. The delayed
this.setState(...)
would then execute and the DOM would be updated again, that time with old users and old questions. The result would also be visible to users.
=> It is pretty safe (I'm not sure about 100%) to use async
with componentDidMount()
method. I'm a big fan of it and so far I haven't encountered any issues which give me too much headache.
Update April 2020: The issue seems to be fixed in latest React 16.13.1, see this sandbox example. Thanks to @abernier for pointing this out.
I have made some research, and I have found one important difference: React does not process errors from async lifecycle methods.
So, if you write something like this:
componentDidMount()
{
throw new Error('I crashed!');
}
then your error will be caught by the error boundry, and you can process it and display a graceful message.
If we change the code like this:
async componentDidMount()
{
throw new Error('I crashed!');
}
which is equivalent to this:
componentDidMount()
{
return Promise.reject(new Error('I crashed!'));
}
then your error will be silently swallowed. Shame on you, React...
So, how do we process errors than? The only way seems to be explicit catch like this:
async componentDidMount()
{
try
{
await myAsyncFunction();
}
catch(error)
{
//...
}
}
or like this:
componentDidMount()
{
myAsyncFunction()
.catch(()=>
{
//...
});
}
If we still want our error to reach the error boundary, I can think about the following trick:
- Catch the error, make the error handler change the component state
- If the state indicates an error, throw it from the
render
method
Example:
class BuggyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
buggyAsyncfunction(){ return Promise.reject(new Error('I crashed async!'));}
async componentDidMount() {
try
{
await this.buggyAsyncfunction();
}
catch(error)
{
this.setState({error: error});
}
}
render() {
if(this.state.error)
throw this.state.error;
return <h1>I am OK</h1>;
}
}