mcpField
R
react-suspense

react-suspense

github.com/claude-dev-suite开发4639 次安装2 天前更新
代码仓库

关于 react-suspense

面向 React 开发者的 Suspense 实践参考,涵盖延迟加载、异步界面及边界设计,并整理错误处理与常见问题。

主要功能

  1. 01延迟加载组件
  2. 02嵌套 Suspense 边界
  3. 03错误边界处理
  4. 04渐进式内容加载
  5. 05骨架屏加载模式

适用场景

  1. 01拆分仪表盘与设置页面代码
  2. 02加载异步用户资料
  3. 03分阶段显示页面内容

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-suspense

安装后在 Claude Code 中输入/react-suspense确认已加载。

附带文件advanced.md

详细介绍

React Suspense

完整参考:有关流式 SSR、SuspenseList、自定义 Suspense 支持的 Hook、图片加载、基于路由的代码拆分和测试模式,请参阅 advanced.md。

不适合使用此技能的情况

  • 使用 React 17 或更早版本(支持有限)
  • 使用类组件
  • 构建非 React 应用
  • 所有数据都是静态的(没有异步操作)

核心概念

Suspense 让你能够以声明式方式指定等待异步操作时的加载状态:

import { Suspense } from 'react';

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

使用 React.lazy 拆分代码

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>
  );
}
预加载组件
const Dashboard = lazy(() => import('./Dashboard'));

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

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

使用 Suspense 获取数据

使用 TanStack Query
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>
  );
}
使用 React 19 use() Hook
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>
  );
}

嵌套 Suspense 边界

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>
  );
}

使用 Suspense 处理错误边界

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>

渐进式加载

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>
  );
}
使用 Transition 处理更新
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>
    </>
  );
}

骨架屏加载模式

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; }
}
`;

反模式

反模式为什么不好正确做法
在渲染时创建 Promise每次渲染都会创建新 Promise在组件外创建
Suspense 边界过多加载过程过于零散将相关内容分组
边界过少整个应用都会暂停为各个部分添加边界
没有 ErrorBoundary错误会导致应用崩溃在 Suspense 外层包裹 ErrorBoundary
使用通用加载旋转图标用户体验差使用骨架屏加载器

快速故障排查

问题解决方案
无限暂停将 Promise 的创建移到组件外
加载状态闪现显示 fallback 前添加延迟
瀑布式加载并行获取数据
滚动位置丢失使用尺寸相同的骨架屏
错误未被捕获添加 ErrorBoundary 包装器

最佳实践

  • ✅ 在有意义的 UI 边界处放置 Suspense
  • ✅ 使用尺寸与内容匹配的骨架屏加载器
  • ✅ 与 ErrorBoundary 结合,实现完整的错误处理
  • ✅ 对非紧急更新使用 transitions
  • ✅ 根据用户意图(悬停、聚焦)预加载组件
  • ❌ 不要在组件内创建 Promise
  • ❌ 不要使用过多细粒度边界

参考文档

评论

支持 Markdown