react-suspense

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Suspense

React Suspense

Full Reference: See advanced.md for Streaming SSR, SuspenseList, Custom Suspense-Enabled Hooks, Image Loading, Route-Based Code Splitting, and Testing patterns.
完整参考:如需了解流式 SSR、SuspenseList、自定义支持 Suspense 的 Hooks、图片加载、基于路由的代码分割以及测试模式,请查看 advanced.md

When NOT to Use This Skill

不适用此技能的场景

  • Using React 17 or earlier (limited support)
  • Working with class components
  • Building non-React applications
  • All data is static (no async operations)
  • 使用 React 17 或更早版本(支持有限)
  • 处理类组件
  • 构建非 React 应用
  • 所有数据均为静态数据(无异步操作)

Core Concept

核心概念

Suspense lets you declaratively specify loading states while waiting for async operations:
tsx
import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <AsyncComponent />
    </Suspense>
  );
}

Suspense 允许你在等待异步操作时,声明式地指定加载状态:
tsx
import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <AsyncComponent />
    </Suspense>
  );
}

Code Splitting with React.lazy

结合 React.lazy 实现代码分割

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

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  const [view, setView] = useState('dashboard');

  return (
    <div>
      <nav>
        <button onClick={() => setView('dashboard')}>Dashboard</button>
        <button onClick={() => setView('settings')}>Settings</button>
      </nav>

      <Suspense fallback={<PageSkeleton />}>
        {view === 'dashboard' && <Dashboard />}
        {view === 'settings' && <Settings />}
      </Suspense>
    </div>
  );
}
tsx
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  const [view, setView] = useState('dashboard');

  return (
    <div>
      <nav>
        <button onClick={() => setView('dashboard')}>Dashboard</button>
        <button onClick={() => setView('settings')}>Settings</button>
      </nav>

      <Suspense fallback={<PageSkeleton />}>
        {view === 'dashboard' && <Dashboard />}
        {view === 'settings' && <Settings />}
      </Suspense>
    </div>
  );
}

Preloading Components

预加载组件

tsx
const Dashboard = lazy(() => import('./Dashboard'));

const preloadDashboard = () => import('./Dashboard');

function NavLink() {
  return (
    <Link
      to="/dashboard"
      onMouseEnter={preloadDashboard}
      onFocus={preloadDashboard}
    >
      Dashboard
    </Link>
  );
}

tsx
const Dashboard = lazy(() => import('./Dashboard'));

const preloadDashboard = () => import('./Dashboard');

function NavLink() {
  return (
    <Link
      to="/dashboard"
      onMouseEnter={preloadDashboard}
      onFocus={preloadDashboard}
    >
      Dashboard
    </Link>
  );
}

Data Fetching with Suspense

结合 Suspense 实现数据获取

Using TanStack Query

使用 TanStack Query

tsx
import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  return <h1>{user.name}</h1>;
}

function UserPage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}
tsx
import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  return <h1>{user.name}</h1>;
}

function UserPage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

Using React 19 use() Hook

使用 React 19 use() Hook

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

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

function UserPage({ userId }: { userId: string }) {
  const [userPromise] = useState(() => fetchUser(userId));

  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

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

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

function UserPage({ userId }: { userId: string }) {
  const [userPromise] = useState(() => fetchUser(userId));

  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Nested Suspense Boundaries

嵌套 Suspense 边界

tsx
function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <main>
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        <Suspense fallback={<ChartsSkeleton />}>
          <Charts />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <DataTable />
        </Suspense>
      </main>
    </div>
  );
}

tsx
function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <main>
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        <Suspense fallback={<ChartsSkeleton />}>
          <Charts />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <DataTable />
        </Suspense>
      </main>
    </div>
  );
}

Error Boundaries with Suspense

结合 Suspense 使用错误边界

tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

// Reusable wrapper
function AsyncBoundary({
  children,
  fallback,
  errorFallback,
}: {
  children: React.ReactNode;
  fallback: React.ReactNode;
  errorFallback: React.ComponentType<FallbackProps>;
}) {
  return (
    <ErrorBoundary FallbackComponent={errorFallback}>
      <Suspense fallback={fallback}>{children}</Suspense>
    </ErrorBoundary>
  );
}

// Usage
<AsyncBoundary
  fallback={<LoadingSpinner />}
  errorFallback={ErrorFallback}
>
  <AsyncComponent />
</AsyncBoundary>

tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div>
      <h2>出现错误</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

// 可复用的包装组件
function AsyncBoundary({
  children,
  fallback,
  errorFallback,
}: {
  children: React.ReactNode;
  fallback: React.ReactNode;
  errorFallback: React.ComponentType<FallbackProps>;
}) {
  return (
    <ErrorBoundary FallbackComponent={errorFallback}>
      <Suspense fallback={fallback}>{children}</Suspense>
    </ErrorBoundary>
  );
}

// 使用示例
<AsyncBoundary
  fallback={<LoadingSpinner />}
  errorFallback={ErrorFallback}
>
  <AsyncComponent />
</AsyncBoundary>

Progressive Loading

渐进式加载

tsx
function ArticlePage({ articleId }: { articleId: string }) {
  return (
    <article>
      {/* Critical content loads first */}
      <Suspense fallback={<TitleSkeleton />}>
        <ArticleTitle articleId={articleId} />
      </Suspense>

      {/* Content loads next */}
      <Suspense fallback={<ContentSkeleton />}>
        <ArticleContent articleId={articleId} />
      </Suspense>

      {/* Less critical - loads last */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments articleId={articleId} />
      </Suspense>
    </article>
  );
}
tsx
function ArticlePage({ articleId }: { articleId: string }) {
  return (
    <article>
      {/* 关键内容优先加载 */}
      <Suspense fallback={<TitleSkeleton />}>
        <ArticleTitle articleId={articleId} />
      </Suspense>

      {/* 内容随后加载 */}
      <Suspense fallback={<ContentSkeleton />}>
        <ArticleContent articleId={articleId} />
      </Suspense>

      {/* 次要内容最后加载 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments articleId={articleId} />
      </Suspense>
    </article>
  );
}

With Transition for Updates

结合 Transition 处理更新

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

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

  function selectTab(nextTab: string) {
    startTransition(() => setTab(nextTab));
  }

  return (
    <>
      <TabButtons selectedTab={tab} onSelect={selectTab} />

      <div className={isPending ? 'opacity-50' : ''}>
        <Suspense fallback={<TabSkeleton />}>
          {tab === 'about' && <About />}
          {tab === 'posts' && <Posts />}
        </Suspense>
      </div>
    </>
  );
}

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

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

  function selectTab(nextTab: string) {
    startTransition(() => setTab(nextTab));
  }

  return (
    <>
      <TabButtons selectedTab={tab} onSelect={selectTab} />

      <div className={isPending ? 'opacity-50' : ''}>
        <Suspense fallback={<TabSkeleton />}>
          {tab === 'about' && <About />}
          {tab === 'posts' && <Posts />}
        </Suspense>
      </div>
    </>
  );
}

Skeleton Loading Patterns

骨架屏加载模式

tsx
function UserCardSkeleton() {
  return (
    <div className="user-card">
      <div className="skeleton skeleton-avatar" />
      <div className="skeleton skeleton-text" style={{ width: '60%' }} />
      <div className="skeleton skeleton-text" style={{ width: '40%' }} />
    </div>
  );
}

// CSS
const skeletonStyles = `
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
  border-radius: 4px;
}

@keyframes skeleton-loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
`;

tsx
function UserCardSkeleton() {
  return (
    <div className="user-card">
      <div className="skeleton skeleton-avatar" />
      <div className="skeleton skeleton-text" style={{ width: '60%' }} />
      <div className="skeleton skeleton-text" style={{ width: '40%' }} />
    </div>
  );
}

// CSS 样式
const skeletonStyles = `
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
  border-radius: 4px;
}

@keyframes skeleton-loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
`;

Anti-Patterns

反模式

Anti-PatternWhy It's BadCorrect Approach
Creating promises in renderNew promise every renderCreate outside component
Too many Suspense boundariesOver-fragmented loadingGroup related content
Too few boundariesEntire app suspendsAdd boundaries per section
No ErrorBoundaryErrors crash appWrap Suspense in ErrorBoundary
Generic loading spinnersPoor UXUse skeleton loaders
反模式问题所在正确做法
在渲染函数中创建 Promise每次渲染都会生成新的 Promise在组件外部创建 Promise
过多的 Suspense 边界加载状态过于碎片化对相关内容进行分组
过少的边界整个应用都会进入挂起状态为每个区块添加边界
未使用 ErrorBoundary错误会导致应用崩溃将 Suspense 包裹在 ErrorBoundary 中
通用加载动画用户体验不佳使用骨架屏加载器

Quick Troubleshooting

快速故障排除

IssueSolution
Infinite suspendingMove promise creation outside component
Flash of loading stateAdd delay before showing fallback
Waterfall loadingFetch data in parallel
Lost scroll positionUse skeletons with same dimensions
Error not caughtAdd ErrorBoundary wrapper
问题解决方案
无限挂起将 Promise 创建移至组件外部
加载状态闪烁添加延迟后再显示备用UI
瀑布式加载并行获取数据
滚动位置丢失使用与内容尺寸一致的骨架屏
错误未被捕获添加 ErrorBoundary 包装器

Best Practices

最佳实践

  • ✅ Place Suspense at meaningful UI boundaries
  • ✅ Use skeleton loaders matching content dimensions
  • ✅ Combine with ErrorBoundary for complete error handling
  • ✅ Use transitions for non-urgent updates
  • ✅ Preload components on user intent (hover, focus)
  • ❌ Don't create Promises inside components
  • ❌ Don't use too many granular boundaries
  • ✅ 将 Suspense 放置在有意义的UI边界处
  • ✅ 使用与内容尺寸匹配的骨架屏加载器
  • ✅ 结合 ErrorBoundary 实现完整的错误处理
  • ✅ 使用 transitions 处理非紧急更新
  • ✅ 在用户触发操作前(悬停、聚焦)预加载组件
  • ❌ 不要在组件内部创建 Promises
  • ❌ 不要使用过多粒度极细的边界

Reference Documentation

参考文档