首页 / React 19 入门教程 / Suspense 与加载

React 19 入门教程

Suspense 与加载

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

ReactSuspenselazy代码分割加载状态并发渲染

37. Suspense 与加载

本节目标:理解 Suspense 怎么工作,学会用它优雅地处理加载态,不再到处写 if (isLoading)

Suspense 是什么

以前处理加载态,你到处写这种代码:

function Page() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => { /* 请求 */ }, []);

  if (isLoading) return <Spinner />;
  return <Content data={data} />;
}

每个组件都来一遍,又丑又容易出错。Suspense 的思路是:声明式地告诉 React”这里有异步操作,没完成时显示这个 fallback”

<Suspense fallback={<Spinner />}>
  <Page />
</Suspense>

子组件在”等待”什么的时候,React 会自动显示 fallback。等子组件准备好了,再替换成实际内容。

什么能触发 Suspense

Suspense 不是万能的,它只对特定的”等待”有反应:

  1. React.lazy 懒加载的组件 —— 代码还没下载完
  2. use(Promise) —— React 19 的 Hook,读取 Promise 的值
  3. 支持 Suspense 的数据源 —— 比如框架内置的 fetch、TanStack Query 的 suspense 模式

普通的 useEffect + fetch 不会触发 Suspense。这是新手最容易搞混的地方。

Note

Suspense 不是”自动检测所有异步操作”。它需要组件主动”抛出”Promise,React 才能捕获到。

React.lazy 与代码分割

React.lazy 让你按需加载组件,配合 Suspense 显示加载态:

import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart.js'));

function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>
      <Suspense fallback={<p>图表加载中……</p>}>
        <HeavyChart />
      </Suspense>
    </div>
  );
}

import('./HeavyChart.js') 是动态导入,Vite 会把 HeavyChart 拆成单独的 JS 文件。只有用户访问到这个页面时才会下载。

Warning

lazy 必须在组件外部声明,不能在组件函数内部。否则每次渲染都会创建新的组件类型,导致状态丢失。

嵌套 Suspense

Suspense 可以嵌套,每个边界独立管理自己的 fallback:

<Suspense fallback={<p>整个页面加载中……</p>}>
  <Header />
  <main>
    <Suspense fallback={<p>图表加载中……</p>}>
      <Chart />
    </Suspense>
    <Suspense fallback={<p>列表加载中……</p>}>
      <DataList />
    </Suspense>
  </main>
</Suspense>

好处是各模块独立加载,不用等全部数据到齐才显示页面。用户先看到 Header,然后图表和列表陆续出现。

Suspense 与 use Hook 配合

React 19 的 use Hook 是 Suspense 的最佳搭档:

import { use, Suspense } from 'react';

function Message({ messagePromise }) {
  const content = use(messagePromise);
  return <p>{content}</p>;
}

function App() {
  return (
    <Suspense fallback={<p>加载消息中……</p>}>
      <Message messagePromise={fetchMessage()} />
    </Suspense>
  );
}

use(messagePromise) 会暂停组件的渲染,等 Promise 完成后再继续。最近的 Suspense 边界显示 fallback。

Tip

传给 use 的 Promise 必须是缓存过的。每次渲染创建新 Promise 会导致无限循环——组件挂起、重渲染、又创建新 Promise、又挂起。

避免”瀑布”问题

Suspense 虽然好看,但用不好容易产生瀑布——串行加载:

// ❌ 瀑布:等 User 加载完才开始加载 Posts
function Page() {
  return (
    <Suspense fallback={<Spinner />}>
      <User />
      <Posts />
    </Suspense>
  );
}

如果 UserPosts 各自有异步依赖,它们会串行执行。解决办法是并行触发:

// ✅ 并行:两个 Suspense 独立加载
function Page() {
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <User />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </div>
  );
}

或者把 Promise 提前创建,在组件外部就开始请求:

// ✅ 请求在渲染前就开始了
const userPromise = fetchUser();
const postsPromise = fetchPosts();

function Page() {
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <User promise={userPromise} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts promise={postsPromise} />
      </Suspense>
    </div>
  );
}

Suspense 的 fallback 细节

fallback 可以是任何 JSX:

<Suspense fallback={
  <div className="skeleton">
    <div className="skeleton-line" />
    <div className="skeleton-line" />
    <div className="skeleton-line" />
  </div>
}>
  <HeavyComponent />
</Suspense>

但注意:fallback 本身不能包含 Suspense 内容。如果 fallback 也需要异步加载,React 会报错。

什么时候不该用 Suspense

Suspense 不是银弹。以下场景用传统 loading state 更合适:

  • 表单提交:用户点提交按钮后等待响应,用 isPendinguseFormStatus 更合适
  • 搜索输入:用户每敲一个键就发请求,用 useDeferredValue 防抖更合适
  • 局部更新:页面大部分已显示,只更新一小块数据,用局部 loading

Suspense 最适合的场景是路由切换首屏加载——整块内容需要等数据到了才能渲染。

Tip

React 19 的 useTransition 配合 Suspense 可以做到”不显示 fallback,保持旧内容直到新内容就绪”。详见 §39。