在 useEffect 中使用异步函数指南
在 useEffect 中使用异步函数指南
useEffect
通常是 React 中进行数据获取的地方。数据获取意味着使用异步函数,在 useEffect
中使用它们可能不像你想象的那么简单。看完本文,你将会真正掌握在useEffect
中使用异步函数的正确姿态。
1. 错误方式
在 useEffect
中有一个错误的数据获取方法。如果你编写以下代码,你的 Linter 将向你发错警告:
// ❌ 不能这么做
useEffect(async () => {
const data = await fetchData();
}, [fetchData])
这里的问题是 useEffect
的第一个参数应该是一个函数,它要么不返回任何东西(未定义),要么返回一个函数(以清除副作用)。但是 async
函数返回 Promise
,它不能作为函数调用。这不是 useEffect
hook 所期望的第一个参数。
那么如何在 useEffect
中使用异步代码呢?
2. 在 useEffect 中写入异步函数
通常解决方案是简单地在 useEffect
内部编写数据获取代码,像这样:
useEffect(() => {
// 声明数据获取函数
const fetchData = async () => {
const data = await fetch('https://yourapi.com');
}
// 调用函数
fetchData()
// 确保捕获任何错误
.catch(console.error);
}, [])
需要注意的一点是,如果希望使用来自异步代码的结果,那么应该在 fetchData
函数内部而不是外部执行。例如,以下情况会导致问题:
useEffect(() => {
// 声明异步数据获取函数
const fetchData = async () => {
// 从 API 获取数据
const data = await fetch('https://yourapi.com');
// 将数据转换为 json
const json = await data.json();
return json;
}
// 调用函数
const result = fetchData()
// 确保捕获任何错误
.catch(console.error);
// ❌ 不要这样做,它不会像你期望的那样运行
setData(result);
}, [])
当调用 setData(result)
行时,你能猜出 result
变量是什么吗?
理解这一点的一种方法是编写一个只等待一定时间的异步函数。
useEffect(() => {
// 声明异步数据获取函数
const fetchData = async () => {
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//等待 1000ms
await sleep(1000);
return 'Hello World';
};
const result = fetchData()
// 确保捕获任何错误
.catch(console.error);
// 输出结果是什么?
console.log(result);
}, [])
输出结果是什么?
如果你答对了,恭喜你!result
将保存一个 pending
状态的 Promise
对象。在控制台中,你会看到如下内容:
Promise {<pending>}
那么如何在 useEffect
中使用异步代码的结果呢?答案是在获取数据函数内部。要修改上面的例子,你可以这样做:
useEffect(() => {
// 声明异步数据获取函数
const fetchData = async () => {
// 从 API 获取数据
const data = await fetch('https://yourapi.com');
// 将数据转换为 json
const json = await response.json();
// 使用 result 设置状态
setData(json);
}
// 调用函数
fetchData()
// 确保捕获任何错误
.catch(console.error);;
}, [])
3. 如果需要从 useEffect 外部提取函数该怎么办?
在某些情况下,你希望在 useEffect
之外有数据获取函数。在这些情况下,你只需要小心地用 useCallback
包装函数。
为什么?因为函数是在 useEffect
之外声明的,所以必须把它放在 hook 的依赖数组中。但是如果函数没有被包装在 useCallback
中,它将在每次重新渲染时更新,从而在每次重新渲染时触发 useEffect
。这不是我们想要的结果。
// 声明异步数据获取函数
const fetchData = useCallback(async () => {
const data = await fetch('https://yourapi.com');
setData(data);
}, [])
// useEffect 只在适当的时候调用 fetchData
useEffect(() => {
fetchData()
// 确保捕获任何错误
.catch(console.error);;
}, [fetchData])
在前面展示了一个在 useEffect
中获取数据的示例,有一个点需要注意:
useEffect(() => {
// 声明异步数据获取函数
const fetchData = async () => {
// 从 API 获取数据
const data = await fetch('https://yourapi.com');
// 将数据转换为 json
const json = await response.json();
// 使用 result 设置状态
setData(json);
}
// 调用函数
fetchData()
// 确保捕获任何错误
.catch(console.error);;
}, [])
你通常需要一种能够取消 setData
调用的方法。在上面的例子中,它是没有用的,因为调用只执行了一次,但我们假设调用依赖于一个参数 param
:
useEffect(() => {
// 声明异步数据获取函数
const fetchData = async () => {
// 从 API 获取数据
const data = await fetch(`https://yourapi.com?param=${param}`);
// 将数据转换为 json
const json = await response.json();
// 使用 result 设置状态
setData(json);
}
// 调用函数
fetchData()
// 确保捕获任何错误
.catch(console.error);;
}, [param])
如果 param
改变了值,fetchData
将被调用两次。如果这发生得很快,就有可能出现一个竞争条件,即第一个调用在第二个调用之后解决,因此状态将保持旧的值。
解决这个问题的方法是使用一个变量来控制是否更新状态。
useEffect(() => {
let isSubscribed = true;
// 声明异步数据获取函数
const fetchData = async () => {
// 从 API 获取数据
const data = await fetch(`https://yourapi.com?param=${param}`);
// 将数据转换为 json
const json = await response.json();
// 如果 'issubscriptions' 为 true,则用结果设置 state
if (isSubscribed) {
setData(json);
}
}
// 调用函数
fetchData()
// 确保捕获任何错误
.catch(console.error);;
// 取消 setData
return () => isSubscribed = false;
}, [param])
这是在可能多次触发 useEffect
中获取数据时非常常见的模式。