最近踩到了一个坑,属于以前没有碰到过的问题,就是在本地测试的时候,docker 的 API 不知道为什么突然有了延迟,以至于在状态更新的时候,之前调用的数据重写了后来调用的数据。
本地写了一个可以复刻这个情况的代码,服务端:
const express = require('express');
const cors = require('cors');
// remove cors error
const corsOptions = {
origin: 'http://localhost:3001',
credentials: true, //access-control-allow-credentials:true
optionSuccessStatus: 200,
};
const app = express();
app.use(cors(corsOptions));
// respond with "hello world" when a GET request is made to the homepage
app.get('/products/1', (req, res) => {
setTimeout(() => {
res.json({
id: 1,
});
}, 1000);
});
app.get('/products/2', (req, res) => {
res.json({
id: 2,
});
});
app.listen(3030, () => {
console.log('port listening to 3030');
});
本来想找个 Mock API 用的,不过没有延迟的设置,所以只能这么做了(叹气)。这个 dummy 服务端是只能接受 /products/1 /products/2 这两个 endpoints,对于 demo 来说够用了。主要用到的 packages 就 express 和 cors,如果想跑,就 init 一个项目直接 npm i 上面俩包就行。
客户端:
import { useEffect, useState } from 'react';
import './App.css';
function App() {
const [timer, setTimer] = useState(1);
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`http://localhost:3030/products/${timer}`, {
signal: controller.signal,
})
.then((data) => {
if (data.ok) return data.json();
})
.then((res) => {
console.log(res);
setData(res);
})
.catch((e) => {
console.log(e);
});
}, [timer]);
return (
<div className="App">
<input
type="text"
value={timer}
onChange={(e) => {
setTimer(e.target.value);
}}
/>
<br />
{JSON.stringify(data)}
</div>
);
}
export default App;
效果如下:

尽管输入的 id 是 2,理论上来说我想显示的内容就是 2,不过因为 1 有延迟,所以延迟的数据重写了本来应有的数据,导致渲染是正常的,数据是异常的这种事情。
这也说明了之前的项目有可能会出现同样的问题(心虚),这也算是切身体会了,对于所有 useEffect 中的异步操作,清理都是非常必要的事情。
解决方案也比较简单,fetch 支持 AbortController,在清理函数中调用即可。
实现效果如下:

代码如下:
// 其他一致
useEffect(() => {
const controller = new AbortController();
fetch(`http://localhost:3030/products/${timer}`, {
signal: controller.signal,
})
.then((data) => {
if (data.ok) return data.json();
})
.then((res) => {
console.log(res);
setData(res);
})
.catch((e) => {
console.log(e);
});
// 主要就是这里
return () => {
console.log('aborted');
controller.abort();
};
}, [timer]);
axios 部分也可以一样使用——axios 在 v0.22.0 之后就支持 AbortController 去取消 API 的调用:
useEffect(() => {
const controller = new AbortController();
axios
.get(`/products/${timer}`, { signal: controller.signal })
.then((data) => {
setData(data);
});
return () => {
console.log('aborted');
controller.abort();
};
}, [timer]);
关于 Axios 其他的简易封装,可以参考这篇:axios 的简易封装,这里不多赘述。
初始化是必须的,不初始化会导致出错……至于为什么……我还得找下资料……
这个可运行的案例写的……真的是为了这碟醋,特地包了这盘饺子