react-suspense

v2026.09.24

React Suspense for data fetching, code splitting, and async operations. Covers Suspense boundaries, lazy loading, streaming SSR, Error Boundaries, suspense-enabled data libraries, and progressive loading patterns. USE WHEN: user mentions "Suspense", "lazy loading", "React.lazy", "code splitting", "streaming SSR", "loading states", asks about "async components", "fallback UI" DO NOT USE FOR: React 17 and earlier (limited Suspense support), Class components, Non-React frameworks

GitHub
安装命令
npx skhub add claude-dev-suite/react-suspense
Markdown
SKILL.md

React Suspense

Full Reference: See advanced.md for Streaming SSR, SuspenseList, Custom Suspense-Enabled Hooks, Image Loading, Route-Based Code Splitting, and Testing patterns.

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)

Core Concept

Suspense lets you declaratively specify loading states while waiting for async operations:

import { Suspense } from 'react';

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

Code Splitting with 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>
  );
}

Preloading Components

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

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

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

Nested Suspense Boundaries

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

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>

Progressive Loading

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

With Transition for Updates

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

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

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

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

Reference Documentation

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/frontend-frameworks/react-suspense

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1