Skip to content
⚠️ This article was written in 2021. Some content may be outdated.

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