学习路径 高级

Suspense 深入

学习如何使用 Suspense 处理异步操作,优化加载体验

Suspense 概述

Suspense 让组件"等待"某些操作完成,期间显示加载状态。它是 React 并发特性的核心,用于处理数据获取、代码分割等异步操作。

核心概念

  • 声明式加载状态:无需手动管理 isLoading 状态
  • 自动处理:React 自动处理数据获取和渲染
  • 并发渲染:利用 React 18 的并发特性
  • 可组合:可以嵌套多个 Suspense
TypeScript
import { Suspense } from 'react';

function DataComponent() {
  const data = fetchData();  // 抛出 Promise 时 Suspense 会捕获
  return <div>{data.name}</div>;
}

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

Suspense 基础用法

包裹异步组件

TypeScript
import { Suspense } from 'react';

// 异步组件
function UserProfile({ userId }: { userId: number }) {
  const user = fetchUser(userId);  // 假设这个函数返回数据或抛出 Promise

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// 使用 Suspense
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

嵌套 Suspense

TypeScript
function App() {
  return (
    <div className="layout">
      {/* 外层 Suspense */}
      <Suspense fallback={<PageSkeleton />}>
        <Header />

        <div className="content">
          {/* 内层 Suspense */}
          <Suspense fallback={<SidebarSkeleton />}>
            <Sidebar />
          </Suspense>

          <main>
            <Suspense fallback={<ContentSkeleton />}>
              <Content />
            </Suspense>
          </main>
        </div>

        <Suspense fallback={<FooterSkeleton />}>
          <Footer />
        </Suspense>
      </Suspense>
    </div>
  );
}
嵌套 Suspense 的优势
  • 独立加载:每个部分独立显示加载状态
  • 渐进式渲染:内容准备好就立即显示
  • 更好的用户体验:不会让整个页面等待

Suspense 与数据获取

Suspense 需要与支持 Promise 的数据获取库配合使用。

创建数据获取工具

TypeScript
// 创建资源函数
function createResource<T>(fetcher: () => Promise<T>) {
  let status: 'pending' | 'success' | 'error' = 'pending';
  let result: T;
  let error: Error;

  const promise = fetcher()
    .then(data => {
      status = 'success';
      result = data;
    })
    .catch(err => {
      status = 'error';
      error = err;
    });

  return {
    read(): T {
      if (status === 'pending') {
        throw promise;  // 抛出 Promise 让 Suspense 捕获
      }
      if (status === 'error') {
        throw error;
      }
      return result;
    }
  };
}

// 使用
const userResource = createResource(() =>
  fetch('/api/user/1').then(res => res.json())
);

function UserProfile() {
  const user = userResource.read();
  return <div>{user.name}</div>;
}

使用 Relay/SWR

TypeScript
// 使用 Relay (内置 Suspense 支持)
import { graphql, usePreloadedQuery } from 'react-relay';

function UserProfile({ queryRef }) {
  const data = usePreloadedQuery(
    graphql`
      query UserProfileQuery {
        user(id: "1") {
          name
          email
        }
      }
    `,
    queryRef
  );

  return <div>{data.user.name}</div>;
}

// 使用 SWR 2.0+ (支持 Suspense)
import useSWR from 'swr';

function UserProfile() {
  const { data } = useSWR('/api/user/1', fetcher, {
    suspense: true  // 启用 Suspense 模式
  });

  return <div>{data.name}</div>;
}

实际示例:用户列表

TypeScript
// 资源缓存
const resourceCache = new Map<string, any>();

function fetchUsers() {
  const cacheKey = 'users';

  if (resourceCache.has(cacheKey)) {
    return resourceCache.get(cacheKey);
  }

  const resource = createResource(() =>
    fetch('/api/users').then(res => res.json())
  );

  resourceCache.set(cacheKey, resource);
  return resource;
}

function UserList() {
  const users = fetchUsers().read();

  return (
    <ul>
      {users.map((user: User) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function App() {
  return (
    <Suspense fallback={<div>加载用户列表...</div>}>
      <UserList />
    </Suspense>
  );
}

Suspense 与代码分割

React.lazy 和 Suspense 配合使用,可以实现组件级别的代码分割。

基础代码分割

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

// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Chart = lazy(() => import('./Chart'));
const Editor = lazy(() => import('./Editor'));

function Dashboard() {
  const [activeTab, setActiveTab] = useState('overview');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('overview')}>概览</button>
        <button onClick={() => setActiveTab('chart')}>图表</button>
        <button onClick={() => setActiveTab('editor')}>编辑器</button>
      </nav>

      <Suspense fallback={<div>加载中...</div>}>
        {activeTab === 'overview' && <HeavyComponent />}
        {activeTab === 'chart' && <Chart />}
        {activeTab === 'editor' && <Editor />}
      </Suspense>
    </div>
  );
}

错误边界集成

TypeScript
import { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

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

function App() {
  return (
    <ErrorBoundary
      fallback={
        <div>
          加载失败,请刷新页面重试
          <button onClick={() => window.location.reload()}>刷新</button>
        </div>
      }
    >
      <Suspense fallback={<Spinner />}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}
代码分割最佳实践
  • 按路由分割:不同页面的组件分开加载
  • 按功能分割:大型组件按功能模块分割
  • 按条件分割:不常用的功能延迟加载
  • 预加载:使用 webpackPrefetch 预加载

Suspense 与错误边界

Suspense 只处理加载状态,错误需要通过 Error Boundary 处理。

创建错误边界

TypeScript
interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  ErrorBoundaryState
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

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

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>出错了!</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

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

使用 react-error-boundary

TypeScript
import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: {
  error: Error;
  resetErrorBoundary: () => void;
}) {
  return (
    <div role="alert">
      <h2>出错了!</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

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

Suspense 与并发渲染

React 18 引入的并发特性让 Suspense 更加强大。

useTransition 优化列表过滤

TypeScript
import { useState, useTransition, Suspense } from 'react';

function FilterableList({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');
  const [isPending, startTransition] = useTransition();

  const filteredItems = items.filter(item =>
    item.name.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={(e) => {
          // 紧急更新:立即更新输入框
          setFilter(e.target.value);

          // 非紧急更新:延迟过滤
          startTransition(() => {
            // 过滤操作
          });
        }}
      />

      <Suspense fallback={<div>加载中...</div>}>
        {isPending && <div className="skeleton">...</div>}
        <List items={filteredItems} />
      </Suspense>
    </div>
  );
}

useDeferredValue 延迟昂贵计算

TypeScript
import { useState, useDeferredValue, Suspense } from 'react';

function SearchResults({ query }: { query: string }) {
  // 延迟更新搜索结果
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(() =>
    searchProducts(deferredQuery),
    [deferredQuery]
  );

  return (
    <Suspense fallback={<div>搜索中...</div>}>
      <ResultList results={results} />
    </Suspense>
  );
}

Suspense List

TypeScript
import { Suspense } from 'react';

// React 18 不再需要 SuspenseList
// 直接嵌套 Suspense 即可

function UserProfile() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <Content />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  );
}
并发渲染的优势
  • 可中断渲染:高优先级更新可以打断低优先级渲染
  • 保持响应:UI 始终响应用户交互
  • 优化体验:优先处理用户关心的问题

Suspense 与 Server Components

React Server Components (RSC) 完全依赖 Suspense 处理流式渲染。

TypeScript
// Server Component (服务端组件)
async function UserProfile({ userId }: { userId: string }) {
  // 直接 await 数据
  const user = await fetchUser(userId);
  const posts = await fetchUserPosts(userId);

  return (
    <div>
      <h1>{user.name}</h1>

      {/* 嵌套 Suspense 边界 */}
      <Suspense fallback={<div>加载文章中...</div>}>
        <PostsList posts={posts} />
      </Suspense>
    </div>
  );
}

// 客户端组件
'use client';

import { Suspense } from 'react';

function Page() {
  return (
    <Suspense fallback={<div>加载用户信息中...</div>}>
      <UserProfile userId="1" />
    </Suspense>
  );
}

Streaming SSR

Suspense 支持 Streaming SSR,服务端可以渐进式发送 HTML。

TypeScript
// Node.js 服务端
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(
    <App />,
    {
      // shell 先渲染
      onShellReady() {
        res.statusCode = 200;
        res.setHeader('Content-type', 'text/html');
        pipe(res);
      },
      // 所有内容渲染完成
      onAllReady() {
        // 如果需要等待所有内容再发送
      },
      // 错误处理
      onError(error) {
        console.error(error);
      }
    }
  );
});
Streaming SSR 的优势
  • 更快的 TTFB: 尽快发送 HTML
  • 渐进式渲染: 内容准备好就发送
  • 更好的用户体验: 用户看到内容更快

Suspense 最佳实践

  1. 提供有意义的 fallback
TypeScript
// ❌ 不好
<Suspense fallback={<div>...</div>}>

// ✅ 好
<Suspense fallback={
  <div className="skeleton">
    <div className="skeleton-avatar" />
    <div className="skeleton-text" />
    <div className="skeleton-text" />
  </div>
}>
  1. 细粒度 Suspense
TypeScript
// ❌ 不好:整个页面一个 Suspense
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Content />
  <Comments />
</Suspense>

// ✅ 好:每个部分独立 Suspense
<Suspense fallback={<HeaderSkeleton />}><Header /></Suspense>
<Suspense fallback={<ContentSkeleton />}><Content /></Suspense>
<Suspense fallback={<CommentsSkeleton />}><Comments /></Suspense>
  1. 总是配合 Error Boundary
TypeScript
<ErrorBoundary fallback={<ErrorUI />}>
  <Suspense fallback={<LoadingUI />}>
    <AsyncComponent />
  </Suspense>
</ErrorBoundary>
  1. 避免过度使用

    • 不是所有异步操作都需要 Suspense。简单的 loading 状态用 useState 即可。
  2. 缓存资源

    • 避免重复请求相同数据,使用缓存机制。

常见问题

Suspense 不工作?

可能的原因:

  • 组件没有抛出 Promise
  • 使用的数据获取库不支持 Suspense
  • Suspense 位置不正确

如何获取加载进度?

Suspense 本身不提供进度信息。可以使用 onLoading prop 或第三方库:

TypeScript
// 使用 startTransition 配合
const [isPending, startTransition] = useTransition();

startTransition(() => {
  // 异步操作
});

Suspense 和 useEffect?

useEffect 在 Suspense 之后执行,即使数据还未加载完成。注意可能的竞态条件。

相关资源