如何处理useEffect中的“竞争条件”的影响?
在使用useEffect请求数据的时候,响应数据可能会以不同的顺序到达。
比如:一个组件接受一个id作为prop,使用id在useEffect中获取数据,可能会遇到一些奇怪的事情,比如组件有时能正确显示数据,有时展示的却是过时的旧数据。
这时候我们就遇到了race condition竞争条件,组件会根据首先完成的请求显示对应结果,在useEffect中获取数据时,如果id更改足够快,该组件就具有了竞争条件:
import React, { useEffect, useState } from 'react';
export default function DataView(props) {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
setTimeout(async () => {
const response = await fetch(`https://xxx/api/book/${props.id}`);
const newData = await response.json();
setData(newData);
}, Math.round(Math.random() * 3000));
}
fetchData();
}, [props.id])
if (!data) return null;
return <div>{data.name}</div>
}
上面的代码片段中在每个fetchData请求中添加了一个3s左右的随机延迟,如果外部props.id的值变更速度够快的情况下,该组件会发出多个请求,这些请求随机无序完成,组件的UI展示结果将是最后完成请求的结果。
修复useEffect的竞争条件
诀窍:利用useEffect的clean-up清理功能
- 使用一个布尔标志,通过条件判断只呈现最后一个结果
- 使用AbortController终止过期请求
使用带有布尔标志的清理函数
import React, { useEffect, useState } from 'react';
export default function DataView(props) {
const [data, setData] = useState(null);
useEffect(() => {
let active = true;
const fetchData = async () => {
setTimeout(async () => {
const response = await fetch(`https://xxx/api/book/${props.id}`);
const newData = await response.json();
if (active) {
setData(newData);
}
}, Math.round(Math.random() * 3000));
}
fetchData();
return () => {
active = false;
}
}, [props.id])
if (!data) return null;
return <div>{data.name}</div>
}
useEffect清理函数会在组件卸载之前运行,以防止内存泄漏,此外,如果一个组件多次渲染,则在下一个效果之前会清除前一个效果,在上面的代码中:
- 更改
props.id会导致重新渲染 - 每次重新渲染会触发清理函数执行,将
active设置为false active为false时,过时的请求响应数据就无法更新我们的组件状态
使用AbortController的清理函数
import React, { useEffect, useState } from 'react';
export default function DataView(props) {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
setTimeout(async () => {
try {
const response = await fetch(`https://xxx/api/book/${props.id}`,{
signal: abortController.signal,
});
const newData = await response.json();
setData(newData);
} catch (error) {
if (error.name === 'AbortError') {
// aborting a fetch throws an error
//
}
}
}, Math.round(Math.random() * 3000));
}
fetchData();
return () => {
abortController.abort();
}
}, [props.id])
if (!data) return null;
return <div>{data.name}</div>
}
- 在
useEffect顶部初始化AbortController - 将
AbortController.signal传递给fetch的options参数 - 捕获抛出的AbortError,当调用
abort()时,fetch()会reject并返回AbortError
虽然在useEffect中请求数据是一种很流行的方式,但是不推荐,因为直接在Effect中编写数据请求会显得重复,并且很难添加缓存和服务端渲染等优化。