Avoiding Race Conditions when Fetching Data with React Hooks
source link: https://www.tuicool.com/articles/hit/YnURbyi
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
About a month ago, I posted an example of fetching data using React Hooks to Twitter. While it was well-intended, Dan Abromov (of the React core team) let me know that my implementation contained a race condition. Consequently, I promised to write a blog post correcting my implementation. This is that post!
Note:If this article helps you, please help spread the word by lending a clap (or 50)! :clap::clap:
Setup
In our example app, we are going to fake-load people’s profile data when their names are clicked. To help visualize the race condition, we’ll create a fakeFetch
function that implements a random delay between 0 and 5 seconds.
const fakeFetch = person => { return new Promise(res => { setTimeout(() => res(`${person}'s data`), Math.random() * 5000); }); };
Initial Implementation
Our initial implementation will use buttons to set the current profile. We reach for the useState
hook to implement this, maintaining the following states:
person data loading
We additional use the useEffect
hook, which performs our fake fetch whenever person
changes.
import React, { Fragment, useState, useEffect } from 'react';
const fakeFetch = person => { return new Promise(res => { setTimeout(() => res(`${person}'s data`), Math.random() * 5000); }); };
const App = () => { const [data, setData] = useState(''); const [loading, setLoading] = useState(false); const [person, setPerson] = useState(null);
useEffect(() => { setLoading(true); fakeFetch(person).then(data => { setData(data); setLoading(false); }); }, [person]);
return ( <Fragment> <button onClick={() => setPerson('Nick')}>Nick's Profile</button> <button onClick={() => setPerson('Deb')}>Deb's Profile</button> <button onClick={() => setPerson('Joe')}>Joe's Profile</button> {person && ( <Fragment> <h1>{person}</h1> <p>{loading ? 'Loading...' : data}</p> </Fragment> )} </Fragment> ); }; export default App;
If we run our app and click one of the buttons, our fake fetch loads data as expected.
Hitting the race condition
The trouble comes when we start switching between people in quick succession. Given the fact that our fake fetch has a random delay, we soon find that our fetch results may be returned out of order. Additionally, our selected profile and loaded data can be out of sync. That’s a bad look!
What’s happening here is relatively intuitive: setData(data)
within the useEffect
hook is only called after the fakeFetch
promise is resolved. Whichever promise resolves last will call setData
last, regardless of which button was actually called last.
Canceling previous fetches
We can fix this race condition by “canceling” the setData
call for any clicks that aren’t most recent. We do this by creating a boolean variable scoped within the useEffect
hook and returning a clean-up function from the useEffect
hook that sets this boolean “canceled” variable to true
. When the promise resolves, setData
will only be called if the “canceled” variable is false.
If that description was a bit confusing, the following code sample of the useEffect
hook should help.
useEffect(() => { let canceled = false;
setLoading(true); fakeFetch(person).then(data => { if (!canceled) { setData(data); setLoading(false); } });
return () => (canceled = true); }, [person]);
Even if a previous button click’s fakeFetch
promise resolves later, its canceled
variable will be set to true
and setData(data)
will not be executed!
Let’s take a look at how our new app functions:
Perfect — No matter how many times we click different buttons, we will always only see data associated with the last button click.
Full code
The full code from this blog post can be found below:
import React, { Fragment, useState, useEffect } from 'react';
const fakeFetch = person => { return new Promise(res => { setTimeout(() => res(`${person}'s data`), Math.random() * 5000); }); };
const App = () => { const [data, setData] = useState(''); const [loading, setLoading] = useState(false); const [person, setPerson] = useState(null);
useEffect(() => { let canceled = false;
setLoading(true); fakeFetch(person).then(data => { if (!canceled) { setData(data); setLoading(false); } });
return () => (canceled = true); }, [person]);
return ( <Fragment> <button onClick={() => setPerson('Nick')}>Nick's Profile</button> <button onClick={() => setPerson('Deb')}>Deb's Profile</button> <button onClick={() => setPerson('Joe')}>Joe's Profile</button> {person && ( <Fragment> <h1>{person}</h1> <p>{loading ? 'Loading...' : data}</p> </Fragment> )} </Fragment> ); }; export default App;
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK