首页 / React 19 入门教程 / 数据请求基础

React 19 入门教程

数据请求基础

本教程共 50 篇 · 第 35 篇 · 更新于 2026-07-29 · 约 8 分钟阅读

React数据请求useEffectfetch竞态条件async

35. 数据请求基础

本节目标:学会在组件里正确地发数据请求,管好加载态和错误态,避开竞态条件这个坑。

为什么要在 Effect 里请求数据

React 组件的渲染必须是纯函数——只负责算出 JSX,不能干”请求网络”这种事。

所以数据请求要放到副作用(effect)里。useEffect 在浏览器绘制完之后执行,正好适合做网络请求。

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('请求失败');
        return res.json();
      })
      .then(data => setUser(data))
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false));
  }, [userId]);

  if (isLoading) return <p>加载中……</p>;
  if (error) return <p>出错了:{error}</p>;
  return <h1>{user.name}</h1>;
}

这个模式你应该记住:三个 state —— 数据、加载态、错误态。Effect 里根据 userId 的变化重新请求。

Note

依赖数组里放 userId,意味着切换用户时会重新发请求。不放依赖数组就是只请求一次。

async/await 的写法

上面的 Promise 链可以改成 async/await,更好读:

useEffect(() => {
  async function loadUser() {
    setIsLoading(true);
    setError(null);
    try {
      const res = await fetch(`/api/users/${userId}`);
      if (!res.ok) throw new Error('请求失败');
      const data = await res.json();
      setUser(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  }
  loadUser();
}, [userId]);

注意:useEffect 的回调函数不能直接写成 async,因为 async 函数会返回 Promise,而 React 要么期望你返回 undefined,要么返回清理函数。所以要在 Effect 内部再包一个 async 函数来调用。

竞态条件:最容易踩的坑

想象一下这个场景:你在筛选列表,快速切换选项。每次切换都发一个请求。问题是——后发的请求可能比先发的先回来。

先点 Alice,再点 Bob,再点 Taylor。如果 Taylor 的响应比 Bob 先到,页面会短暂显示 Taylor,然后又被 Bob 的响应覆盖。这就是竞态条件。

// 有 bug 的版本
useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [userId]);

修复办法是用清理函数标记”过期”的请求:

useEffect(() => {
  let cancelled = false;

  async function loadUser() {
    setIsLoading(true);
    try {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) setUser(data);
    } catch (err) {
      if (!cancelled) setError(err.message);
    } finally {
      if (!cancelled) setIsLoading(false);
    }
  }

  loadUser();

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

每次 Effect 重新执行,清理函数会把上一次标记为 true。这样过时的响应即使回来也不会更新 state。

Warning

我见过很多人在 Effect 里直接写 fetch 但忘了处理竞态条件。测试环境网速快可能测不出来,生产环境就露馅了。

用 AbortController 取消请求

ignore 标记只能阻止 setState,请求本身还在跑。想真正取消请求,用 AbortController

useEffect(() => {
  const controller = new AbortController();

  async function loadUser() {
    try {
      const res = await fetch(`/api/users/${userId}`, {
        signal: controller.signal,
      });
      const data = await res.json();
      setUser(data);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
      }
    }
  }

  loadUser();

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

controller.abort() 会中止正在进行的 fetch。fetch 被中断后会抛出 AbortError,我们判断一下忽略掉就行。

抽成自定义 Hook

数据请求的逻辑到处重复,不如抽成一个自定义 Hook:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    const controller = new AbortController();

    async function load() {
      setIsLoading(true);
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (err) {
        if (!cancelled && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (!cancelled) setIsLoading(false);
      }
    }

    load();

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

  return { data, isLoading, error };
}

用起来就很干净了:

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useFetch(`/api/users/${userId}`);

  if (isLoading) return <p>加载中……</p>;
  if (error) return <p>出错了:{error}</p>;
  return <h1>{user.name}</h1>;
}

什么时候不该用 Effect 请求数据

在 Effect 里写 fetch 是最基础的方式,但也有不少弊端:

  • 服务端渲染(SSR)时 Effect 不执行,HTML 里只有 loading
  • 父子组件各自请求容易形成”网络瀑布”
  • 没有缓存,组件重新挂载就得重新请求

React 19 推荐用 use Hook 或框架内置的数据获取方案(比如 React Router 的 loader、Next.js 的服务端组件)。这些内容后面章节会讲。

Tip

如果你只是写个简单的客户端应用,用 Effect 请求就够了。一旦涉及 SSR 或复杂数据依赖,考虑上 TanStack Query 或 SWR 这类库。