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

React Concurrent Mode 深入理解

React 在 16.3 版本引入了 Fiber 架構,為併發渲染打下了基礎。Concurrent Mode 是 React 團隊正在開發的一項實驗性功能,它讓 React 可以同時準備多個版本的 UI,從根本上改善使用者體驗。本文將深入探討 Concurrent Mode 的工作原理和使用方式。

為什麼需要 Concurrent Mode

在傳統的 React 渲染模式下,一旦開始渲染,就會同步地完成整棵元件樹的渲染。對於大型應用,這個過程可能會阻塞主執行緒幾十甚至幾百毫秒,導致使用者互動無響應:

jsx
// 傳統模式下的問題
function SearchResults({ query }) {
  // 假設 results 有 10000 條資料
  const results = expensiveFilter(query);

  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>
          <ResultItem data={item} />
        </li>
      ))}
    </ul>
  );
}

// 當用戶在輸入框中輸入時
function App() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        // 每次輸入都需要等待 SearchResults 渲染完成
        // 輸入會出現明顯延遲
      />
      <SearchResults query={query} />
    </div>
  );
}

Concurrent Mode 允許 React 中斷耗時的渲染,優先處理使用者互動。

開啟 Concurrent Mode

Concurrent Mode 目前是實驗性功能,需要使用特殊的 API 建立 Root:

jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

// 傳統模式
// ReactDOM.render(<App />, document.getElementById('root'));

// Concurrent Mode
const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<App />);

Suspense for Data Fetching

Concurrent Mode 最重要的配套功能是 Suspense,它不僅可以處理程式碼分割,還可以處理資料載入:

jsx
import React, { Suspense } from 'react';

// 建立一個簡單的資料讀取器
function createResource(fetcher) {
  let status = 'pending';
  let result;

  const promise = fetcher().then(
    data => {
      status = 'success';
      result = data;
    },
    error => {
      status = 'error';
      result = error;
    }
  );

  return {
    read() {
      if (status === 'pending') throw promise;
      if (status === 'error') throw result;
      if (status === 'success') return result;
    }
  };
}

// 建立使用者資料資源
const userResource = createResource(() =>
  fetch('/api/user/1').then(res => res.json())
);

// 元件直接讀取資料,不處理載入狀態
function UserProfile() {
  // 如果資料未就緒,會丟擲 Promise,由最近的 Suspense 捕獲
  const user = userResource.read();

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

// 外層用 Suspense 包裹
function App() {
  return (
    <Suspense fallback={<div>載入使用者資訊...</div>}>
      <UserProfile />
    </Suspense>
  );
}

useTransition

useTransition 是 Concurrent Mode 提供的最核心 Hook,它可以將某些狀態更新標記為"過渡"(transition),這些更新可以被中斷:

jsx
import React, { useState, useTransition, Suspense } from 'react';

function App() {
  const [query, setQuery] = useState('');
  const [resource, setResource] = useState(null);

  // isPending 表示過渡是否還在進行中
  const [startTransition, isPending] = useTransition({
    timeoutMs: 3000
  });

  function handleSearch(e) {
    const value = e.target.value;

    // 立即更新輸入框(高優先順序)
    setQuery(value);

    // 將搜尋請求標記為過渡(低優先順序)
    startTransition(() => {
      setResource(fetchSearchResults(value));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} />

      {/* isPending 可以在載入期間顯示額外的 UI 反饋 */}
      {isPending && <Spinner />}

      <Suspense fallback={<div>搜尋中...</div>}>
        {resource && <SearchResults resource={resource} />}
      </Suspense>
    </div>
  );
}

useTransition 的優先順序機制

使用者輸入 "react"
  │
  ├─ 立即更新 input 值(高優先順序,同步)
  │   輸入框立即顯示 "react"
  │
  └─ startTransition 更新搜尋結果(低優先順序,可中斷)
      React 可以中斷這個渲染來處理新的輸入
      渲染完成後顯示結果

SuspenseList

SuspenseList 控制多個 Suspense 邊界的顯示順序:

jsx
import React, { Suspense, SuspenseList } from 'react';

function App() {
  return (
    <SuspenseList revealOrder="forwards">
      <Suspense fallback={<Spinner />}>
        <UserProfile />
      </Suspense>
      <Suspense fallback={<Spinner />}>
        <UserPosts />
      </Suspense>
      <Suspense fallback={<Spinner />}>
        <UserFollowers />
      </Suspense>
    </SuspenseList>
  );
}

revealOrder 的可選值:

  • 'forwards':按順序顯示,前面的載入完才顯示後面的
  • 'backwards':反向順序
  • 'together':全部載入完一起顯示

帶快取的資料獲取模式

結合 Suspense 可以實現優雅的資料快取:

js
// cache.js
function createCache() {
  const cache = new Map();

  return {
    get(key, fetcher) {
      if (cache.has(key)) {
        const entry = cache.get(key);
        if (entry.status === 'pending') throw entry.promise;
        if (entry.status === 'error') throw entry.error;
        return entry.data;
      }

      const promise = fetcher(key).then(
        data => {
          cache.set(key, { status: 'success', data });
        },
        error => {
          cache.set(key, { status: 'error', error });
        }
      );

      cache.set(key, { status: 'pending', promise });
      throw promise;
    },

    invalidate(key) {
      cache.delete(key);
    }
  };
}

export const userCache = createCache();
jsx
// UserCard.jsx
import { userCache } from './cache';

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then(res => res.json());
}

function UserCard({ userId }) {
  const user = userCache.get(userId, fetchUser);

  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
    </div>
  );
}

與傳統模式的對比

| 特性 | 傳統模式 | Concurrent Mode | | ------|---------|----------------| | 渲染方式 | 同步、不可中斷 | 非同步、可中斷 | | 使用者互動 | 渲染期間阻塞 | 優先響應互動 | | 載入狀態 | 各元件自行管理 | Suspense 統一管理 | | 程式碼分割 | React.lazy | React.lazy + Suspense | | 資料獲取 | useEffect + setState | Suspense + 資源讀取 |

當前狀態與注意事項

Concurrent Mode 目前仍是實驗性功能:

  1. API 可能會有變化
  2. 不建議在生產環境使用
  3. 需要 react@experimental 版本
  4. 部分第三方庫可能不相容
bash
npm install react@experimental react-dom@experimental

小結

  • Concurrent Mode 讓 React 可以中斷渲染,優先處理使用者互動
  • useTransition 標記低優先順序的狀態更新
  • Suspense 統一管理載入狀態,包括資料獲取
  • SuspenseList 控制多個非同步元件的顯示順序
  • 需要使用 createRoot API 開啟
  • 目前仍處於實驗階段,API 可能變化
  • 代表了 React 未來的發展方向:宣告式非同步 UI

MIT Licensed