Prompts-MCP 686 skills · ai / design / design-pattern / framework / fundamentals / habit / lang / tech-selection /mcp/sse
framework framework/react/reliability/race-condition-cancellation.examples.md medium

race-condition-cancellation.examples

React · 异步竞态与请求取消 · 完整示例

两种解法各一份完整可运行写法。自写 fetch 时二选一即可;首选仍是 TanStack Query。

方案 A · useEffect + AbortController 真取消

适用:取数由依赖(关键词 / 页码)驱动,希望旧请求在网络层就被中断。

import { useEffect, useState } from "react";
import { Input, List } from "antd";

interface Item {
  id: string;
  title: string;
}

// 取数下沉为纯函数:接收 signal,旧请求被 abort 后这里会抛 AbortError
async function fetchSearch(keyword: string, signal: AbortSignal): Promise<Item[]> {
  const res = await fetch(`/api/search?kw=${encodeURIComponent(keyword)}`, { signal });
  return res.json();
}

export function SearchBox() {
  const [keyword, setKeyword] = useState("");
  const [list, setList] = useState<Item[]>([]);

  useEffect(() => {
    // 空关键词不发请求,早返回
    if (!keyword) {
      setList([]);
      return;
    }

    // 为本次请求建专属 controller
    const controller = new AbortController();

    // 取数:拿到结果才 setState;被取消时进 catch 不动状态
    fetchSearch(keyword, controller.signal)
      .then(setList)
      .catch((err) => {
        // 被新输入取消的请求抛 AbortError,静默忽略,保留最新结果
        if (err.name !== "AbortError") throw err;
      });

    // cleanup:依赖变化或卸载时中断上一次请求,网络层真取消
    return () => controller.abort();
  }, [keyword]);

  return (
    <>
      <Input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="搜索" />
      <List dataSource={list} renderItem={(it) => <List.Item key={it.id}>{it.title}</List.Item>} />
    </>
  );
}

方案 B · stale-result guard 忽略非最新响应

适用:底层 SDK 不支持 AbortSignal,或滚动翻页这类无法取消但能比对序号的场景。

import { useRef, useState, useCallback } from "react";
import { Button, List } from "antd";

interface Item {
  id: string;
  title: string;
}

// 取数下沉:返回某一页数据,page2 可能晚于 page3 到达
async function fetchPage(page: number): Promise<Item[]> {
  const res = await fetch(`/api/items?page=${page}`);
  return res.json();
}

export function InfiniteList() {
  const [list, setList] = useState<Item[]>([]);
  const [page, setPage] = useState(1);
  // 递增序号:每次请求自增,响应回来比对,只有最新序号的结果才采纳
  const seqRef = useRef(0);

  const loadMore = useCallback(async () => {
    // 计算下一页页码
    const nextPage = page + 1;
    // 本次请求领一个递增序号,并记为当前最新
    const mySeq = ++seqRef.current;
    // 取数
    const items = await fetchPage(nextPage);
    // 守卫:若期间已有更新的请求发出,本次为陈旧结果,丢弃
    if (mySeq !== seqRef.current) return;
    // 采纳:追加新一页并推进页码
    setList((prev) => [...prev, ...items]);
    setPage(nextPage);
  }, [page]);

  return (
    <>
      <List dataSource={list} renderItem={(it) => <List.Item key={it.id}>{it.title}</List.Item>} />
      <Button onClick={loadMore}>加载更多</Button>
    </>
  );
}