大约一个月前,我在Twitter上发布了一个使用React Hooks获取数据的例子。虽然这个例子的初衷是好的,但Dan Abromov(React核心团队的)让我知道,我的实现包含了一个竞赛条件。因此,我承诺要写一篇博文,纠正我的实现。这就是那篇博文!
设置
在我们的例子中,当人们的名字被点击时,我们将假意加载他们的资料数据。为了帮助可视化竞赛条件,我们将创建一个fakeFetch 函数,实现0到5秒的随机延迟。
const fakeFetch = (person) => {
return new Promise((res) => {
setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
});
};
初始实现
我们的初始实现将使用按钮来设置当前的个人资料。我们达到useState 钩子来实现这一点,维护以下状态。
person, 用户选择的人data, 根据所选的人,从我们的假取件中加载的数据loading, 当前是否正在加载数据
我们另外使用useEffect 钩子,每当person 变化时,它就会执行我们的假取款。
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;
如果我们运行我们的应用程序并点击其中一个按钮,我们的假取数就会按照预期加载数据。
遇到竞赛条件
当我们开始在人与人之间快速切换时,麻烦就来了。鉴于我们的假取款有一个随机的延迟,我们很快发现我们的取款结果可能会不按顺序返回。此外,我们选择的资料和加载的数据也可能不同步。这是一个不好的现象!
这里发生的事情是相对直观的:useEffect 钩子中的setData(data) 只在fakeFetch 承诺被解决后才被调用。无论哪个承诺最后解决,都会最后调用setData ,而不管哪个按钮实际上最后被调用。
取消之前的提取
我们可以通过 "取消 "对任何非最近的点击的setData 调用来解决这个竞赛条件。我们通过在useEffect 钩子中创建一个布尔变量,并从useEffect 钩子中返回一个清理函数,将这个布尔 "canceled "变量设置为true 。当承诺解决时,只有当 "canceled "变量为假时,setData 才会被调用。
如果这个描述有点令人困惑,下面的useEffect 钩子的代码示例应该会有所帮助。
useEffect(() => {
let canceled = false;
setLoading(true);
fakeFetch(person).then((data) => {
if (!canceled) {
setData(data);
setLoading(false);
}
});
return () => (canceled = true);
}, [person]);
即使之前的按钮点击的fakeFetch 承诺后来解决了,它的canceled 变量将被设置为true ,而setData(data) 将不会被执行!
让我们来看看我们的新应用程序是如何运作的。
完美--无论我们点击不同的按钮多少次,我们将永远只看到与最后一次点击按钮相关的数据。
完整的代码
这篇博文的完整代码可以在下面找到。
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;