React 在 16.3 版本引入了 Fiber 架構,為併發渲染打下了基礎。Concurrent Mode 是 React 團隊正在開發的一項實驗性功能,它讓 React 可以同時準備多個版本的 UI,從根本上改善使用者體驗。本文將深入探討 Concurrent Mode 的工作原理和使用方式。
為什麼需要 Concurrent Mode
在傳統的 React 渲染模式下,一旦開始渲染,就會同步地完成整棵元件樹的渲染。對於大型應用,這個過程可能會阻塞主執行緒幾十甚至幾百毫秒,導致使用者互動無響應:
// 傳統模式下的問題
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:
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,它不僅可以處理程式碼分割,還可以處理資料載入:
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),這些更新可以被中斷:
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 邊界的顯示順序:
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 可以實現優雅的資料快取:
// 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();
// 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 目前仍是實驗性功能:
- API 可能會有變化
- 不建議在生產環境使用
- 需要
react@experimental版本 - 部分第三方庫可能不相容
npm install react@experimental react-dom@experimental
小結
- Concurrent Mode 讓 React 可以中斷渲染,優先處理使用者互動
useTransition標記低優先順序的狀態更新- Suspense 統一管理載入狀態,包括資料獲取
SuspenseList控制多個非同步元件的顯示順序- 需要使用
createRootAPI 開啟 - 目前仍處於實驗階段,API 可能變化
- 代表了 React 未來的發展方向:宣告式非同步 UI