讲解

Suspense 是 React 的「异步编排」机制:组件在渲染期间如果发现数据还没准备好,就「挂起」(抛出一个 Promise),React 向上找到最近的 <Suspense> 边界,显示它的 fallback;Promise 就绪后重新尝试渲染。开发者写的组件代码是「同步风格」的——const data = use(query) 直接拿到结果,加载态由边界统一声明,不再每个组件手写 isLoading 三元。

Suspense 的战略价值在「边界的摆放」。一个 <Suspense> 包住整页:任何数据没好就全页转圈(粗粒度,体验差)。按区块摆边界:头部、列表、侧边栏各自独立加载,谁先好谁先显示(细粒度,渐进式体验)。配合路由级代码分割,懒加载的组件代码下载期间也是 Suspense 接管——const Admin = lazy(() => import('./Admin'))<Suspense fallback={...}> 是标准用法。

现实提醒:Suspense 的数据加载需要和「Suspense 感知」的数据源配合——直接在 useEffect 里 fetch 不会触发 Suspense。官方推荐的路径是框架集成(Next.js 的 async 组件)或支持 Suspense 的库(TanStack Query 的 suspense 模式、SWR、Relay),以及 React 19 的 use() 配合缓存的 Promise。手写一个「Promise 缓存 + throw promise」的玩具实现有助于理解,但生产代码别自己造。

示例

手写一个迷你 Suspense 数据源:throw promise 协议 + Promise 缓存(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// Suspense 感知的数据源:第一次读抛 Promise,就绪后返回数据
function createResource(fetcher) {
  let status = 'pending';
  let result;
  const promise = fetcher().then(
    (data) => {
      status = 'success';
      result = data;
    },
    (err) => {
      status = 'error';
      result = err;
    },
  );
  return {
    read() {
      if (status === 'pending') throw promise; // 挂起协议:抛 Promise
      if (status === 'error') throw result; // 错误边界接管
      return result;
    },
  };
}

// 模拟 React 的渲染循环:挂起就等 Promise,然后重试
async function renderWithSuspense(readFn) {
  for (;;) {
    try {
      return readFn(); // 读到了 → 渲染成功
    } catch (p) {
      if (p instanceof Promise) {
        await p; // 挂起 → 等待 → 重试渲染
        continue;
      }
      throw p;
    }
  }
}

let fetchCount = 0;
const resource = createResource(
  () =>
    new Promise((resolve) => {
      fetchCount++;
      setTimeout(() => resolve({ title: '深入 React' }), 50);
    }),
);

// 第一次 read 会 throw promise
assert.throws(() => resource.read(), (e) => e instanceof Promise);

const data = await renderWithSuspense(() => resource.read());
assert.strictEqual(data.title, '深入 React');
assert.strictEqual(fetchCount, 1); // 缓存:重试渲染不会重新请求

// 之后再读同步返回
assert.strictEqual(resource.read().title, '深入 React');

console.log('首次渲染挂起,50ms 后数据就绪重试成功:', JSON.stringify(data));
console.log('Promise 缓存保证只请求一次:', fetchCount === 1);

真实代码(React 19 + use):

import { Suspense, lazy, use } from 'react';

// 路由级懒加载:下载期间 Suspense 接管
const Admin = lazy(() => import('./Admin'));

// 细粒度边界:各区块独立加载
function Page({ bookPromise }) {
  return (
    <>
      <h1>书店</h1>
      <Suspense fallback={<p>书籍加载中…</p>}>
        <BookDetail promise={bookPromise} />
      </Suspense>
      <Suspense fallback={<p>评论加载中…</p>}>
        <Comments />
      </Suspense>
    </>
  );
}

function BookDetail({ promise }) {
  const book = use(promise); // 渲染期间读 Promise,没好就挂起
  return <h2>{book.title}</h2>;
}

常见坑

  • 在 useEffect 里 fetch 却期待 Suspense:effect 数据流不挂起,需要 Suspense 感知的数据源(use/框架/查询库)。
  • 一个边界包整页:一个慢接口拖住全页;按区块摆边界做渐进加载。
  • use 的 Promise 每次渲染新建:渲染函数里 use(fetch(...)) 会无限请求-挂起循环;Promise 要缓存或在事件/服务端创建。
  • fallback 闪烁:数据 50ms 就好时闪一下 loading 更难受,可以给 fallback 加延迟或骨架屏。
  • 忘记错误边界:请求失败的 throw error 要由 ErrorBoundary 接,只摆 Suspense 不接错误会白屏。

小结

Suspense = 组件挂起 + 边界显示 fallback + 就绪后重试;粒度决定体验;数据源必须 Suspense 感知。下一章把多页面组织起来:React Router。