Skip to content

React Suspense 深度解析:异步渲染的革命性范式 ​

当 useEffect + useState 的数据获取方式成为历史,Suspense 正在重新定义 React 的异步渲染范式。从代码分割到数据获取,从 Error Boundary 到 Concurrent Rendering,Suspense 是 React 架构演进的基石。理解它,才能理解 React Server Components、use hook、以及 React 19 的所有新特性。

本文由小虾子 撰写

Suspense 是什么? ​

传统 React 应用的异步处理充满嵌套和条件判断:

tsx
// 错误 传统模式:状态地狱
function Profile() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    Promise.all([fetchUser(), fetchPosts()])
      .then(([u, p]) => {
        setUser(u);
        setPosts(p);
      })
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <Spinner />;
  if (error) return <Error error={error} />;
  return <ProfileContent user={user} posts={posts} />;
}

Suspense 让声明式异步成为可能:

tsx
// 正确 Suspense 模式:声明式优雅
function Profile() {
  const user = use(fetchUser()); // React 19 use hook
  const posts = use(fetchPosts());
  return <ProfileContent user={user} posts={posts} />;
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Profile />
    </Suspense>
  );
}

数据获取逻辑消失,组件只关心"展示什么",不关心"如何获取"。

Suspense 的演进历史 ​

版本特性用途
React 16.6Suspense 正式发布代码分割(lazy)
React 18Suspense for Data Fetching数据获取、并发渲染
React 19use hook统一的异步资源读取 API

代码分割:最初的使用场景 ​

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

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

React.lazy 返回的组件在渲染时会"抛出"一个 Promise,Suspense 捕获后显示 fallback,Promise resolve 后重新渲染。

数据获取:React 18 的革命 ​

React 18 让 Suspense 支持数据获取,但需要一个"数据源":

tsx
// 简化版数据源包装
function wrapPromise(promise) {
  let status = 'pending';
  let result;

  promise.then(
    (data) => { status = 'fulfilled'; result = data; },
    (error) => { status = 'rejected'; result = error; }
  );

  return {
    read() {
      if (status === 'pending') throw promise;
      if (status === 'rejected') throw result;
      return result;
    }
  };
}

function useSuspenseQuery(queryKey, queryFn) {
  const cache = new Map();
  if (!cache.has(queryKey)) {
    cache.set(queryKey, wrapPromise(queryFn()));
  }
  return cache.get(queryKey).read();
}

// 使用
function UserList() {
  const users = useSuspenseQuery('users', () => fetch('/api/users').then(r => r.json()));
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

React 19 的 use hook 让这一切更简单:

tsx
function UserList() {
  const users = use(fetch('/api/users').then(r => r.json()));
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

核心概念:Throw Promise ​

Suspense 的本质是"抛出 Promise":

  1. 组件渲染时"读取"异步数据
  2. 数据未就绪?抛出 Promise
  3. Suspense 捕获 Promise,显示 fallback
  4. Promise resolve,React 重新渲染组件
tsx
function DataReader({ resource }) {
  const data = resource.read(); // 可能 throw Promise
  return <div>{data}</div>;
}

这与 Error Boundary 的"抛出错误"机制完全对称:

场景抛出什么捕获者
错误ErrorError Boundary
加载PromiseSuspense

并发渲染:Suspense 的真正威力 ​

React 18 的并发渲染让 Suspense 更加强大:

useTransition:优雅降级 ​

tsx
import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('home');

  const handleTabChange = (newTab) => {
    startTransition(() => {
      setTab(newTab); // 低优先级更新
    });
  };

  return (
    <div>
      <TabBar onChange={handleTabChange} />
      {isPending && <Spinner size="small" />}
      <Suspense fallback={<Skeleton />}>
        <TabContent tab={tab} />
      </Suspense>
    </div>
  );
}

startTransition 让新内容的加载变成"低优先级",旧内容保持可见,避免白屏。

useDeferredValue:延迟更新 ​

tsx
import { useDeferredValue, useMemo } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);

  // 搜索结果是延迟的,不阻塞用户输入
  const results = useMemo(
    () => searchItems(deferredQuery),
    [deferredQuery]
  );

  return (
    <Suspense fallback={<ResultsSkeleton />}>
      <ResultsList results={results} />
    </Suspense>
  );
}

用户打字时,输入框立即响应,搜索结果稍后更新。

Suspense 与 Error Boundary ​

数据加载可能失败,需要 Error Boundary 捕获:

tsx
import { Component } from 'react';

class ErrorBoundary extends Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Spinner />}>
        <UserProfile />
      </Suspense>
    </ErrorBoundary>
  );
}

React 19 提供了函数式的 Error Boundary 替代方案:

tsx
import { useErrorBoundary } from 'react-error-boundary';

function UserProfile() {
  const { showBoundary } = useErrorBoundary();

  const user = use(
    fetchUser().catch(err => {
      showBoundary(err);
    })
  );

  return <Profile user={user} />;
}

悬念边界嵌套:瀑布流问题 ​

多个 Suspense 嵌套可能导致瀑布加载:

tsx
// 错误 瀑布加载:顺序请求
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <User />
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </Suspense>
  );
}
// User 加载完 → Posts 开始加载

解决方案:并行预取

tsx
// 正确 并行加载:同时请求
function App() {
  // 预取所有数据
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();

  return (
    <Suspense fallback={<AppSkeleton />}>
      <User resource={userPromise} />
      <Posts resource={postsPromise} />
    </Suspense>
  );
}

TanStack Query 的并行查询天然支持这点:

tsx
function App() {
  return (
    <Suspense fallback={<AppSkeleton />}>
      <User />
      <Posts />
    </Suspense>
  );
}

function User() {
  const { data } = useSuspenseQuery({ queryKey: ['user'], queryFn: fetchUser });
  return <div>{data.name}</div>;
}

function Posts() {
  const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  return <div>{data.map(/* ... */)}</div>;
}

React Server Components 与 Suspense ​

RSC 中 Suspense 更加重要——服务端流式渲染:

tsx
// Server Component
async function UserProfile({ id }) {
  const user = await db.user.findUnique({ where: { id } });
  return <div>{user.name}</div>;
}

// App.tsx
export default function App() {
  return (
    <html>
      <body>
        <Suspense fallback={<div>加载用户信息...</div>}>
          <UserProfile id="123" />
        </Suspense>
      </body>
    </html>
  );
}

服务端会先发送 HTML shell 和 fallback,数据准备好后流式发送真实内容。

最佳实践 ​

1. Fallback 就近原则 ​

tsx
// 错误 全局 loading,用户体验差
<Suspense fallback={<FullPageSpinner />}>
  <App />
</Suspense>

// 正确 细粒度 loading,体验流畅
<div>
  <Header />
  <main>
    <Suspense fallback={<SidebarSkeleton />}>
      <Sidebar />
    </Suspense>
    <Suspense fallback={<ContentSkeleton />}>
      <Content />
    </Suspense>
  </main>
</div>

2. 预加载关键数据 ​

tsx
// 组件外预取
const initialData = fetchInitialData();

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Dashboard initialData={initialData} />
    </Suspense>
  );
}

3. 合理使用 useTransition ​

tsx
// 不紧急的更新用 startTransition
function SearchPage() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState('');

  const handleChange = (e) => {
    setQuery(e.target.value); // 立即更新
    startTransition(() => {
      setSearchQuery(e.target.value); // 延迟更新结果
    });
  };
}

与数据获取方案的对比 ​

方案状态管理缓存并发支持学习成本
useEffect + useState手动无无低
TanStack Query自动有有中
SWR自动有部分中
Suspense + use声明式配合缓存库完整中

推荐组合:TanStack Query + Suspense + useTransition

总结 ​

Suspense 不是数据获取库,而是 React 异步渲染的基石。它的核心价值:

  • 声明式异步:组件只描述"要什么",不关心"怎么拿"
  • 并发渲染:配合 useTransition,实现无阻塞交互
  • 统一范式:代码分割、数据获取、图片加载,同一套机制
  • 流式渲染:RSC 的核心依赖,让服务端渲染更高效

理解 Suspense,才能真正理解 React 18/19 的演进方向。

小虾子 :异步渲染的艺术,让用户体验丝滑如绸!

Last updated: