useEffect 支持 async/await

在 useEffect 中正确使用异步函数的方法

问题

在 useEffect 回调函数中直接使用 async/await 会报错,因为 async 函数返回 Promise,而 useEffect 期望返回一个清理函数或 undefined。

解答

为什么不支持 async

useEffect 的返回值应该是一个清理函数,执行时机如下:

  • 首次渲染不执行清理,在下一次渲染时清除上一次的副作用
  • 组件卸载时执行清理操作

如果返回值是异步的,无法预知代码执行情况,容易出现难以定位的 Bug。因此 React 直接限制了 useEffect 回调函数不能是 async 函数。

解决方案

方法一:在内部创建异步函数

useEffect(() => {
  const asyncFun = async () => {
    setPass(await mockCheck());
  };
  asyncFun();
}, []);

方法二:使用 IIFE

useEffect(() => {
  (async () => {
    setPass(await mockCheck());
  })();
}, []);

自定义 Hook

可以封装成自定义 Hook,参考 ahooks 的 useAsyncEffect 实现:

function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  deps?: DependencyList,
) {
  useEffect(() => {
    let cancelled = false;
    
    const execute = async () => {
      await effect();
    };
    
    execute();
    
    return () => {
      cancelled = true;
    };
  }, deps);
}

这里的 cancelled 标识符用于中断执行。当用户频繁操作时,如果上一轮操作还未完成就开始下一轮,可以通过检查 cancelled 来停止已失效的操作。

清理机制的正确实现

不应该依赖异步函数来实现清理机制,而应该使用取消机制:

function useAsyncEffect(
  effect: (isCanceled: () => boolean) => Promise<void>,
  dependencies?: any[]
) {
  return useEffect(() => {
    let canceled = false;
    effect(() => canceled);
    return () => {
      canceled = true;
    };
  }, dependencies);
}

使用示例:

useAsyncEffect(async (isCanceled) => {
  const result = await doSomeAsyncStuff(stuffId);
  if (!isCanceled()) {
    // 仍然可以安全地执行副作用
  }
}, [stuffId]);

这种方式通过传入 isCanceled 函数,让异步逻辑可以在关键节点检查是否已被取消,避免在 Hook 取消后仍对外部状态产生影响。

关键点

  • useEffect 不支持 async 回调函数,因为需要返回清理函数而非 Promise
  • 解决方法是在 useEffect 内部创建并调用异步函数
  • 可以封装成自定义 Hook(如 useAsyncEffect)来优化使用体验
  • 使用取消标识符(如 cancelled)来中断已失效的异步操作
  • 清理机制应该是同步的取消机制,而不是依赖异步函数的延迟清理