避免在使用 React Hooks 获取数据时出现竞态条件
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
React useEffectHook 非常适合在函数式组件中执行副作用。一个常见的例子就是获取数据。但是,如果不注意清理副作用,就可能导致竞态条件!在本文中,我们将确保正确清理副作用,从而避免竞态条件问题。
设置
在我们的示例应用中,我们将模拟用户点击姓名时加载其个人资料数据。为了便于理解竞态条件,我们将创建一个fakeFetch函数,该函数会随机延迟 0 到 5 秒。
const fakeFetch = person => {
return new Promise(res => {
setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
});
};
初步实施
我们最初的实现方案将使用按钮来设置当前配置文件。useState为此,我们调用了钩子函数,并维护以下状态:
person用户选择的人data根据所选人员,从我们的模拟获取中加载数据。loading当前是否正在加载数据
我们还使用了useEffecthook,它会在每次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;
如果我们运行应用程序并点击其中一个按钮,我们的模拟获取操作会按预期加载数据。
触发竞态条件
问题在于当我们快速切换用户时。由于我们的模拟数据获取存在随机延迟,我们很快就会发现获取结果的顺序可能被打乱。此外,我们选择的个人资料和加载的数据也可能不同步。这可就糟糕了!
这里发生的事情相对来说比较直观:setData(data)钩子useEffect函数只有在fakeFetchPromise 被解析后才会调用。setData无论实际最后点击的是哪个按钮,最后解析的 Promise 都会调用 `last` 函数。
取消之前的获取操作
我们可以通过“取消”setData所有非最近点击的调用来解决这个竞态条件。具体做法是,在useEffect钩子函数内部创建一个布尔变量,并在钩子函数中返回一个清理函数,该函数useEffect会将这个名为“canceled”的布尔变量设置为 false true。这样,当 Promise 解析时,setData只有当“canceled”变量为 false 时,才会调用该清理函数。
如果上述描述有些令人困惑,以下useEffect钩子代码示例应该会有所帮助。
useEffect(() => {
let canceled = false;
setLoading(true);
fakeFetch(person).then(data => {
if (!canceled) {
setData(data);
setLoading(false);
}
});
return () => (canceled = true);
}, [person]);
即使先前按钮点击的fakeFetchPromise 稍后解析,其canceled变量也会被设置为 false 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;

