React 18で導入された並行処理(Concurrent)機能は、レンダリングの仕組みを根本的に変革しました。UIの複数バージョンを同時に準備し、進行中の処理を中断し、緊急度の高い更新を優先的に処理できるようになります。宣言的なプログラミングモデルを維持しながら、より応答性の高いアプリケーションを実現します。
Reactにおける並行処理は、すべてを一度に有効化するモードではありません。実験的なConcurrent Modeとは異なり、React 18以降の並行機能はオプトイン方式です。必要な機能を必要な箇所から段階的に導入します。この仕組みを支えるのがFiberアーキテクチャです。Reactのレンダリングフェーズは一時停止と再開が可能で、優先順位に応じて異なる処理単位を切り替えながら実行できます。
useTransitionフック
useTransitionは、状態更新を非緊急(トランジション)としてマークします。これにより、ユーザーのタイピングなどの重要な操作中にレンダリングがメインスレッドをブロックするのを防ぎます。
import { useTransition } from "react";
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
function handleChange(e) {
const value = e.target.value;
setQuery(value);
startTransition(() => {
setFilter(value);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<SearchResults filter={filter} />
</div>
);
}
isPendingフラグは、UIをブロックせずにローディングインジケーターを表示する手段を提供します。トランジションはsetTimeoutによるデバウンスとは根本的に異なります。トランジションは実行を遅延させるのではなく、レンダリングの優先度を下げます。CPUが空いていれば即座に実行され、負荷が高ければ中断されて後回しになります。
useDeferredValueフック
useTransitionが状態セッターをラップするのに対し、useDeferredValueは値をラップします。状態更新そのものをラップできない場面で有用です。
import { useDeferredValue, useMemo } from "react";
function SearchResults({ query, data }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filtered = useMemo(
() => expensiveFilter(data, deferredQuery),
[deferredQuery, data]
);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{filtered.map((item) => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
遅延値は固定遅延ではなく、CPUがアイドルになった時点で即座に更新されます。これにより「古い状態」を表示し続けるパターンが可能になります。新しい結果の計算中も以前の結果が薄く表示され、UIが真っ白になることはありません。
| 手法 | レイテンシ | ユーザー体験 |
|---|---|---|
| 同期的レンダリング | 全キー入力でフル実行 | フィルタ中にUIが固まる |
| デバウンス(300ms) | 固定300ms遅延 | 入力が遅れて感じられる |
useDeferredValue | 空きCPU時は即時 | 滑らかな入力、スムーズな遷移 |
Suspenseと並行レンダリングの統合
並行レンダリング下でのSuspenseは、従来の同期的なモデルよりもはるかに強力です。従来は1つのコンポーネントがサスペンドするとツリー全体がフォールバックに切り替わりましたが、並行Suspenseでは兄弟コンポーネントは操作可能なままです。
function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<Sidebar />
<Suspense fallback={<DetailSkeleton />}>
<MainContent />
</Suspense>
</Suspense>
);
}
Suspense境界をネストすることで、ローディング状態の粒度を細かく制御できます。サイドバーは即座に表示され、メインコンテンツはストリーミングで後から到着します。startTransitionと組み合わせれば、前の画面を表示し続けながら次の画面を準備できるため、「ローディングフラッシュ」を完全に排除できます。
フック外でのstartTransition
startTransitionはスタンドアロン関数としても利用可能で、Reactコンポーネントの外部から呼び出せます。これはルーターのイベントハンドラや状態管理ライブラリとの統合に有用です。
import { startTransition } from "react";
import { useNavigate } from "react-router-dom";
function navigateWithTransition(path) {
startTransition(() => {
navigate(path);
});
}
ZustandやReduxのストア更新をstartTransitionでラップすることで、非緊急の状態変更を優先的に後回しにできます。これは多くのサブスクライバーを持つストアで特に効果的です。
レスポンシブ検索コンポーネントの構築
実践的な検索コンポーネントは、すべての並行機能を組み合わせます。入力状態は緊急、フィルタ計算にはuseDeferredValue、詳細ページへのナビゲーションにはstartTransition、検索結果はSuspense境界でラップします。
function SearchContainer() {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input);
function handleClear() {
setInput("");
}
return (
<Suspense fallback={<ResultsSkeleton />}>
<SearchInput
value={input}
onChange={setInput}
onClear={handleClear}
/>
<SearchResults query={deferredInput} />
</Suspense>
);
}
高速タイピング時は最後のキー入力だけが完全なレンダリングをトリガーし、途中の入力はすべて前のトランジションを中断します。「検索がもたつく」問題を根本的に解決します。
テストと移行
並行機能のテストにはReact Testing Libraryとactを使用します。
import { act, render, screen } from "@testing-library/react";
await act(async () => {
render(<SearchPage />);
});
expect(screen.getByText("Loading...")).toBeInTheDocument();
React 18+が必要で、すべてオプトインのため既存コンポーネントに影響はありません。検索入力やタブ切り替えなどの高レイテンシ操作から導入を始め、ナビゲーションやデータローディングへ拡大してください。最適化の前に必ずプロファイリングでボトルネックを特定しましょう。
まとめ
並行機能が提供する3つのツール — useTransition(状態更新の優先度制御)、useDeferredValue(派生計算の遅延)、並行Suspense(段階的ローディング)— を適切に使い分けることで、ヘビーな計算処理下でも即座に反応するUIを実現できます。
