Skip to content

React Suspense 数据获取模式

React 18 即将发布,Suspense 的数据获取模式终于有了官方推荐方案。之前 Suspense 只能做代码分割的 loading 状态,现在可以用于数据获取了。

Suspense 的核心思路

Suspense 不是"loading 状态管理器",而是"异步依赖的协调器"。

jsx
// 传统做法:组件自己管理 loading 状态
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data)
      setLoading(false)
    })
  }, [userId])

  if (loading) return <Spinner />
  return <div>{user.name}</div>
}

// Suspense 做法:数据源抛出 Promise,Suspense 捕获
function UserProfile({ userId }) {
  // read() 会抛出 Promise(pending)或返回数据(resolved)
  const user = userResource.read(userId)
  return <div>{user.name}</div>
}

// 父组件
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  )
}

组件不用关心 loading 状态,由 Suspense 统一处理。

实现一个简单的 Suspense 数据源

typescript
// utils/createResource.ts
export function createResource<T>(asyncFn: () => Promise<T>) {
  let status: 'pending' | 'success' | 'error' = 'pending'
  let result: T
  let error: Error

  const suspender = asyncFn().then(
    (data) => {
      status = 'success'
      result = data
    },
    (e) => {
      status = 'error'
      error = e
    }
  )

  return {
    read(): T {
      switch (status) {
        case 'pending':
          throw suspender  // Suspense 捕获 Promise
        case 'error':
          throw error      // ErrorBoundary 捕获错误
        case 'success':
          return result
      }
    },
  }
}
jsx
// 使用
import { createResource } from './utils/createResource'

// 创建资源(在组件外部或用 useMemo 缓存)
const userResource = createResource(() => fetch('/api/user/1').then(r => r.json()))

function UserProfile() {
  const user = userResource.read()  // 可能抛出 Promise
  return <div>{user.name}</div>
}

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

结合 React Query(推荐方案)

React Query 3.25+ 已经支持 Suspense 模式:

jsx
import { useQuery } from 'react-query'

function UserProfile({ userId }) {
  const { data: user } = useQuery(
    ['user', userId],
    () => fetchUser(userId),
    {
      suspense: true,  // 开启 Suspense 模式
    }
  )

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

// 布局
function UserPage({ userId }) {
  return (
    <div>
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId={userId} />
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>

      <Suspense fallback={<FollowersSkeleton />}>
        <Followers userId={userId} />
      </Suspense>
    </div>
  )
}

每个区域独立加载,不互相阻塞。

嵌套 Suspense

jsx
function App() {
  return (
    <Suspense fallback={<FullPageSkeleton />}>
      <Header />

      <main>
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />

          <Suspense fallback={<ContentSkeleton />}>
            <MainContent />
          </Suspense>
        </Suspense>
      </main>
    </Suspense>
  )
}

外层 Suspense 是兜底,内层 Suspense 处理局部加载。React 18 的流式 SSR 能逐段发送 HTML。

和 React 18 Suspense SSR 配合

jsx
// 服务端:选择性注水(Selective Hydration)
import { hydrateRoot } from 'react-dom/client'

function App() {
  return (
    <html>
      <body>
        <Suspense fallback={<HeaderSkeleton />}>
          <Header />
        </Suspense>

        <Suspense fallback={<MainSkeleton />}>
          <MainContent />
        </Suspense>
      </body>
    </html>
  )
}

// 服务端先发送 Header 的 HTML
// MainContent 的数据准备好后,流式追加
// 客户端逐段 hydrate,不需要等所有内容

用户更早看到内容,交互也更早可用。

错误处理

jsx
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <p>出错了:{error.message}</p>
          <button onClick={resetErrorBoundary}>重试</button>
        </div>
      )}
    >
      <Suspense fallback={<Spinner />}>
        <UserProfile userId={1} />
      </Suspense>
    </ErrorBoundary>
  )
}

Suspense 处理 loading,ErrorBoundary 处理错误,职责清晰。

注意事项

jsx
// ❌ Suspense 内不要有条件渲染的数据获取
function Bad({ showProfile }) {
  return (
    <Suspense fallback={<Spinner />}>
      {/* 条件切换时可能触发意外的 Suspense */}
      {showProfile ? <UserProfile /> : <GuestView />}
    </Suspense>
  )
}

// ✅ 用 key 强制重新创建
function Good({ showProfile, userId }) {
  return (
    <Suspense fallback={<Spinner />}>
      {showProfile
        ? <UserProfile key={userId} userId={userId} />
        : <GuestView />}
    </Suspense>
  )
}

小结

  • Suspense 数据获取的核心:组件抛出 Promise,Suspense 捕获并显示 fallback
  • 推荐用 React Query / SWR 等库的 Suspense 模式,不用自己实现
  • 嵌套 Suspense 实现逐区域加载,配合 React 18 流式 SSR 效果最佳
  • Suspense + ErrorBoundary:loading 和错误处理职责分离
  • React 18 正式版发布后,这个模式会成为主流

MIT Licensed