对于一个复杂的 React 项目,为了维护方便我们会把与后端 API 交互的逻辑封装在一个单独的 .js 文件中,例如 dataProvider.js。
如下代码示例使用了 axios 从 algolia 获取含有指定关键字的文章列表:
// dataProvider.js
import axios from "axios";
export function getDataFromServer(query) {
return axios("https://hn.algolia.com/api/v1/search?query=" + query);
}
然后在组件中可以导入此文件,我们在 useEffect 中调用上述的 getDataFromServer 函数,并使用 useState hook 更新组件使用的状态。
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
// 导入 dataProvider.js
import { getDataFromServer } from "./dataProvider";
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState("react"); // 初始关键字
useEffect(async () => {// <= 使用 useEffect Hook,注意这里的关键字 async
getDataFromServer(query).then((reponseData) => {
setData(reponseData.data);
});
}, [query]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<SearchResults />, rootElement);
组件看起来可以正常运行,不过在开发者工具控制台中会看到如下警告信息:
Warning: An Effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, you may write an async function separately and then call it from inside the effect:
async function fetchComment(commentId) {
// You can await here
}
useEffect(() => {
fetchComment(commentId);
}, [commentId]);
In the future, React will provide a more idiomatic solution for data fetching that doesn't involve writing effects manually.
in SearchResults (at src/index.js:31)
React 作为一个成熟的库给出详细的调试信息,它告诉我们在 Effect 函数如果有返回值的话,只能是一个用于清理副作用的函数。而我们的代码用了 async 关键字,这是不允许的。同时也给了解决办法,可以先写一个单独的 async 函数,然后在 effect 函数内调用它,例如:
async function fetchComment(commentId) {
// You can await here
}
useEffect(() => {
fetchComment(commentId);
}, [commentId]);
按照它的指引,我们可以改写 effect 函数如下:
useEffect(() => {
async function fetchData() {
const result = await getDataFromServer(query);
setData(result.data);
}
fetchData();
}, [query]);
看起来问题解决了。
不过这里有一个隐藏的问题,如果我们在 effect 函数里添加日志就会发现端倪:
useEffect(() => {
async function fetchData() {
console.log("fetch data for keyword:", query);
const result = await getDataFromServer(query);
console.log("set data for ", query);
setData(result.data);
}
fetchData();
}, [query]);
如果我们在输入框内输入“1234567890”,有可能输出如下:
fetch data for keyword: 1
fetch data for keyword: 12
fetch data for keyword: 123
set data for keyword 12
fetch data for keyword: 1234
set data for keyword 123
set data for keyword 1
fetch data for keyword: 12345
set data for keyword 1234
fetch data for keyword: 123456
set data for keyword 12345
fetch data for keyword: 1234567
set data for keyword 123456
set data for keyword 1234567
fetch data for keyword: 12345678
set data for keyword 12345678
fetch data for keyword: 123456789
fetch data for keyword: 1234567890
set data for keyword 123456789
set data for keyword 1234567890
从日志中可以看到发起 API 查询时和输入的顺序保持一致,但是更新状态时却出现了不一致。例如关键字 “123” 的查询结果早于关键字 "1" 的设置了。
原因不难理解:虽然请求可以依次发出,但是网络请求收到网速等因素影响,无法保证响应依次返回。
要解决这个问题,可以判断当前的 API 请求是否已被忽略,如果已忽略则及时响应返回也不再更新状态。
例如:
useEffect(() => {
let ignore = false; // 当前 API 请求是否被忽略,默认不忽略
async function fetchData() {
console.log("fetch data for keyword:", query);
const result = await getDataFromServer(query);
if (!ignore) {
// 判断当前 API 请求是否仍有效果
console.log("set data for keyword", query);
setData(result.data);
} else {
console.log("ignore data for keyword", query);
}
}
fetchData();
return () => {
// 如果发生 clean up ,则忽略本次 API 请求
ignore = true;
};
}, [query]);
这里利用了 JavaScript 的闭包特性保存 API 请求的有效性,同时使用了 useEffect 的 cleanup 功能更新它的有效性。如果一个新的 API 请求发出,则上次的 API 请求的 ignore 会置为 true,即使响应返回也不会更新应用的状态了。
再次输入“1234567890”,可能的输出如下:
fetch data for keyword: 1
fetch data for keyword: 12
fetch data for keyword: 123
ignore data for keyword 12
fetch data for keyword: 1234
fetch data for keyword: 12345
ignore data for keyword 1
fetch data for keyword: 123456
ignore data for keyword 123
fetch data for keyword: 1234567
ignore data for keyword 123456
ignore data for keyword 1234
fetch data for keyword: 12345678
ignore data for keyword 1234567
ignore data for keyword 12345
fetch data for keyword: 123456789
ignore data for keyword 12345678
fetch data for keyword: 1234567890
ignore data for keyword 123456789
set data for keyword 1234567890
可以看到 setData 函数只为最后输入的关键字执行了一次,其他的均被忽略。问题解决了。
至此应用状态更新次序的问题解决了,不过还有一个小问题,那就是虽然忽略了状态不必要的更新,但 API 请求依然每次输入更新时都发出了,这些请求对于服务器端来说造成不必要的压力,我们可以继续优化,取消那些无谓的请求。
Axios 自 v0.22.0 开始支持 AbortController 可以取消网络请求。它的用法如下:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
我们首先修改 dataProvider:
// dataProvider.js
import axios from "axios";
export function getDataFromServer(query, requestOptions) {
return axios(
"https://hn.algolia.com/api/v1/search?query=" + query,
requestOptions // 允许使用 axios 配置项
);
}
完善 effect 函数:
useEffect(() => {
const controller = new AbortController();
let ignore = false;
async function fetchData() {
console.log("fetch data for keyword:", query);
const result = await getDataFromServer(query, {
signal: controller.signal // <= 传入 axios 的 signal 配置项
});
if (!ignore) {
console.log("set data for keyword", query);
setData(result.data);
} else {
console.log("ignore data for keyword", query);
}
}
fetchData();
return () => {
// 如果发生 clean up ,则忽略本次 API 请求
ignore = true;
controller.abort(); // <= cleanup 时取消 API 请求
};
}, [query]);
此时打印出的日志:
fetch data for keyword: 1
fetch data for keyword: 12
fetch data for keyword: 123
fetch data for keyword: 1234
fetch data for keyword: 12345
fetch data for keyword: 123456
fetch data for keyword: 1234567
fetch data for keyword: 12345678
fetch data for keyword: 123456789
fetch data for keyword: 1234567890
set data for keyword 1234567890
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
添加我为好友,拉您入交流群!
请使用微信扫一扫!