React异步模式Skill react-async-patterns

React 中异步/等待的正确性,使用 Zustand。用于调试竞态条件、遗漏的等待、悬浮承诺或异步定时问题。适用于 React Web 和 React Native。

前端开发 0 次安装 0 次浏览 更新于 3/3/2026

React 异步模式

问题陈述

React 中的异步错误非常隐蔽,因为它们通常在开发中可以工作,但在负载下或在边缘情况下会失败。最常见的问题:遗漏 await 的异步函数,状态更新之间的竞态条件,以及假设操作按顺序完成。


模式:悬浮承诺检测

问题: 调用异步函数而不使用 await 会导致它在后台运行。如果后续代码依赖于其完成,您将获得竞态条件。

// 之前(有bug)- saveData 是异步的但没有被等待
saveData(item);              // 火然后忘记 ❌
await processData(item);     // 在保存完成之前运行

// 之后(已修复)
await saveData(item);        // 等待状态更新 ✅
await processData(item);     // 现在按正确的顺序运行

为什么它很微妙: 两个函数的签名中可能都有 async,但只有一个被等待了。代码乍一看“看起来是对的”。

检测:

# 查找潜在的悬浮承诺 - 没有 await 的异步调用
grep -rn "^\s*[a-zA-Z]*\s*(" --include="*.ts" --include="*.tsx" | \
  grep -v "await\|return\|const\|let\|if\|else\|=>"

预防:

  1. ESLint 规则 @typescript-eslint/no-floating-promises - 在 lint 时捕获此问题
  2. 代码审查触发器:任何调用可能为异步的函数而没有 awaitreturn 或赋值的行

模式:后置条件验证

问题: 假设异步调用成功而没有验证。调用可能会提前返回,静默抛出,或未能更新状态。

// 之前(有bug)- 假设加载工作了
await loadData(id);
// 盲目地进行下一步...

// 之后(防御性)
await loadData(id);
const loaded = useStore.getState().data;
if (Object.keys(loaded).length === 0) {
  throw new Error(
    `Failed to load data for ${id} - cannot proceed`
  );
}

原则: 将每个异步调用视为可能失败,直到证明否则。

何时验证:

  • 在加载后续操作依赖的数据后
  • 在必须完成才能继续的状态更新后
  • 在不可逆操作之前(提交、删除)

模式模板:

await someAsyncOperation();
const result = getRelevantState();
if (!isValid(result)) {
  throw new Error(`[${functionName}] Post-condition failed: ${diagnosticContext}`);
}

模式:异步函数识别

问题: 不是所有异步函数看起来都是异步的。Zustand 动作、回调和返回承诺的函数可能没有明显的 async 关键字。

隐藏的异步模式:

// 明显的异步
async function fetchData() { ... }

// 较不明显 - 返回 Promise
function fetchData(): Promise<Data> { ... }

// 隐藏 - 实际上异步的 Zustand 动作
const useStore = create((set, get) => ({
  // 这看起来是同步的,但内部调用了异步
  enableFeature: (id: string) => {
    someAsyncSetup().then(() => {  // ← 隐藏的异步!
      set({ features: [...get().features, id] });
    });
  },
}));

// 适当的异步 Zustand 动作
const useStore = create((set, get) => ({
  enableFeature: async (id: string) => {
    await someAsyncSetup();
    set({ features: [...get().features, id] });
  },
}));

检测: 检查函数签名和实现:

# 查找返回 Promise 的函数
grep -rn "): Promise<" --include="*.ts" --include="*.tsx"

# 查找可能需要 await 的 .then() 链
grep -rn "\.then(" --include="*.ts" --include="*.tsx"

模式:顺序与并行异步

问题: 当它们可以并行时顺序运行异步操作(慢),或者当它们必须是顺序时并行(竞态条件)。

// 顺序 - 当顺序重要时正确
await stepOne();
await stepTwo();
await stepThree();

// 并行 - 当操作独立时正确
const [user, settings, history] = await Promise.all([
  fetchUser(id),
  fetchSettings(id),
  fetchHistory(id),
]);

// 错误 - 当顺序重要时并行
await Promise.all([
  stepOne(),   // 这些有依赖关系!
  stepTwo(),
]);

决策框架:

操作共享状态? 必须按顺序运行? 模式
Promise.all()
顺序 await
通常按顺序以安全为原则

模式:useEffect 中的异步

问题: useEffect 回调不能直接是异步的。常见的清理和竞态条件错误。

// 错误 - useEffect 不能是异步的
useEffect(async () => {
  const data = await fetchData();
  setData(data);
}, []);

// 正确 - 内部异步函数
useEffect(() => {
  async function load() {
    const data = await fetchData();
    setData(data);
  }
  load();
}, []);

// 更好 - 用于竞态条件的清理
useEffect(() => {
  let cancelled = false;

  async function load() {
    const data = await fetchData();
    if (!cancelled) {
      setData(data);
    }
  }
  load();

  return () => {
    cancelled = true;
  };
}, [dependency]);

// 最好 - 使用 AbortController 进行 fetch
useEffect(() => {
  const controller = new AbortController();

  async function load() {
    try {
      const response = await fetch(url, { signal: controller.signal });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        setError(error);
      }
    }
  }
  load();

  return () => controller.abort();
}, [url]);

模式:React Query / TanStack Query

问题: 手动异步状态管理容易出错。使用库。

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 获取数据
function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error error={error} />;
  return <Profile user={data} />;
}

// 带有缓存失效的变异
function UpdateUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: updateUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate(userData)}>
      保存
    </button>
  );
}

ESLint 配置

添加这些规则以在 lint 时捕获异步问题:

{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/require-await": "warn",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/no-misused-promises": "error"
  }
}

必需: @typescript-eslint/eslint-plugin 和正确的 TypeScript 配置。


代码审查清单

审查异步代码时,请检查:

  • [ ] 每个异步函数调用要么被 await,要么被 return,要么明确地作为火然后忘记,并带有注释
  • [ ] 相互依赖的操作使用 await 进行序列化
  • [ ] 在关键异步操作后验证后置条件
  • [ ] 使用异步的 useEffect 使用内部函数模式
  • [ ] 当组件可能在异步过程中卸载时考虑竞态条件
  • [ ] 异步失败时存在错误处理
  • [ ] 对于应该可以取消的 fetch 调用使用 AbortController

快速调试

当出现异步定时问题时:

// 添加时间戳以追踪执行顺序
console.log(`[${Date.now()}] 开始步骤 1`);
await stepOne();
console.log(`[${Date.now()}] 完成步骤 1`);
console.log(`[${Date.now()}] 开始步骤 2`);
await stepTwo();
console.log(`[${Date.now()}] 完成步骤 2`);

查找:

  • 操作完成的顺序出乎意料
  • 操作在前一个操作完成之前开始
  • 异常快速的“完成”(可能没有等待)