
React19.2の新機能「Activity」について
プログラミング
WebフロントエンドのUIライブラリのReactのversionが、19.2にマイナーアップデートしたことに伴い、新しい「Activity」バウンダリが利用可能となりました。公式サイト
そのActivityの機能をひと言で説明すると、UI の一部を非表示にしたり表示したりするもので、propsは mode で、値は visibility 、hidden のみになります。
そこで、本記事ではその「Activity」についての概要と注意点を中心に整理しました。
なお、本記事を読んで理解できる点は、以下の2点になります。
- Activity によって、従来の条件付きレンダリングで課題となっていた状態管理やパフォーマンスの問題を解決
- Activity 利用時の注意点を理解。特に
Suspenseを利用したデータフェッチやクリーンアップ処理について
記事の構成は以下になります。
- 従来の条件付きレンダリングの課題
- Activityの概要
- Activity(hidden) と useEffect のフェッチ挙動
- Activityでハイドレーションを最適化
- 注意点
従来の条件付きレンダリングの課題
では、本題に入る前に従来のコンポーネントの表示、非表示を制御する条件付きレンダリングについて説明します。
{isVisible && <Sidebar />}
// または
{isVisible ? <Sidebar /> : null}条件付きレンダリングでは、要素がアンマウントされるため、state・副作用・サブスクリプションがすべて破棄され、再表示時に再構築されます。
※サブスクリプションとは、イベントリスナー、setInterval、observer、websocketのsubscribeなどを指します。
具体的な課題
条件付きレンダリングで、stateや副作用が破棄されることで、UIにどのような影響を及ぼすのでしょうか。公式サイトのサンプル例を元に整理します。
1.サイドバーの開閉で、ドロップダウンリストが初期状態に戻る
- サイドバーを展開する
- ドロップダウンリストを開いた状態にする
- サイドバーを閉じる
- 再度、サイドバーを展開する
- ドロップダウンリストが閉じた状態に戻る
本来は、ドロップダウンリストを開いた状態にしたい。
2.フォーム入力中の textareaの値が初期状態に戻る
- お問い合わせページのフォームの
textareaに「test」と入力する - 別ページに遷移する
- 再度、お問い合わせページに戻る
- フォームの
textareaの値が空になり、初期状態に戻る
本来は、textarea に「test」と入力されてほしい。
3.動画プレイヤー再生後に最初の状態に戻る
- 動画プレイヤーを再生する
- 再生途中で別ページに遷移する
- 再度、動画プレイヤーのページに戻る
- 動画プレイヤーが最初の状態になる
本来は、再生途中の状態にしたい。
これらの状態(state)が、Activityを利用することで保持されて期待動作となります。
CSSで非表示(display:none)の限界
条件付きレンダリングの他に、コンポーネントの表示と非表示を制御する方法として、CSSのプロパティ display が挙げられます。
<div style={{ display: isVisible ? 'block' : 'none' }}>
<Sidebar />
</div>そのCSSで制御したときの問題点は以下となります。
- DOM と state は保持され続け、React のツリー上からは除外されない
- props や state の更新が起きるたびに再レンダーが行われ、他のコンポーネントと同様に イベントリスナーやエフェクトが動作し続ける
- よって、CSSで非表示にしたコンポーネントが増えるほど、状態管理・再描画の追跡コストが重くなり、パフォーマンスが指数関数的に低下する
それらの課題を解決するための役割が、React19.2での新しいバウンダリ Activity になります。
Activityの概要
Activityの概要について、公式サイトでは以下のように説明しています。
Activity は、「バックグラウンドアクティビティ」をレンダリングするためのメカニズム。
再び表示される可能性のあるコンテンツを破棄せず、非表示コンテンツに不要な副作用が生じないようにしながら、UIと内部stateを維持・復元できる。
| props | mode |
|---|---|
| 値 | visibility | hidden |
Activity の構造イメージ
<Sidebar />
↑ アンマウントされると state が消える
<Activity mode="hidden">
<Sidebar />
</Activity>
↑ DOM と state は保持されたまま- DOMを保持しつつ「表示状態」をReactが制御
- 非表示時でもstateを保ち、再表示を高速化
比較表は以下になります。
条件付きレンダリング(&&, ?:null)
| 可視切替の仕組み | アンマウント/再マウント |
|---|---|
| state | 破棄 |
| 副作用(Effect)の扱い | アンマウント/再マウント |
| プリレンダー制御 | 無し |
CSS非表示(display:none)
| 可視切替の仕組み | DOMを残して非表示 |
|---|---|
| state | 保持 |
| 副作用(Effect)の扱い | 継続(Effect動作・購読維持) |
| プリレンダー制御 | 無し |
Activity(mode="hidden")
| 可視切替の仕組み | DOM保持・低優先度更新 |
|---|---|
| state | 保持 |
| 副作用(Effect)の扱い | 停止(非表示時にcleanup) |
| プリレンダー制御 | 有り |
Activity(mode="hidden") と useEffect のフェッチ挙動
では、次に mode を hidden にしたときの useEffect の挙動について。hidden 遷移時にクリーンアップが走り、useEffect 内の処理(fetch 等)は実行されません。
function Home() {
useEffect(() => {
fetch('/api/home'); // hidden中は実行されない
return () => console.log('cleanup');
}, []);
}hidden時にクリーンアップ処理が走るため、useEffect内でデータフェッチするのではなく、Suspense とActivity を組み合わせてフェッチ処理を実装することになります。
const homePromise = fetch('/api/home').then(r => r.json());
function Home() {
const data = use(homePromise); // ← Suspense対応。hiddenプリレンダーにも乗る
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
<Suspense fallback={<div>Loading...</div>}>
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}
<Posts />
</Activity>
</Suspense>- hidden中でもプリレンダー対象になるのは Suspense対応データソース(use API)のみ
- useEffect でのfetchは対象外
- Activity を Suspense で Wrapすることで、非表示状態でも先読み(プリレンダー)が可能
Activityでハイドレーションを最適化
Activity は、「どの領域をどのタイミングでインタラクティブ化するか」をReact に伝える ハイドレーション境界の役割も持ちます。
※ハイドレーションとは
サーバーで生成された表示可能な HTML に、クライアント側で「状態・イベント・インタラクティブ性」を付与するプロセスです。
- SSRで生成されたHTMLを保持したまま、どの部分を独立してハイドレーションすべきかを判断
mode="visible":即attach-
mode="hidden":低優先度でattachを遅延
結果、主要UIを優先的に操作可能にするためTTIが改善されます。
TTIとは、読み込みを開始してからユーザーが操作可能になるまでの時間
図解:ハイドレーション最適化の流れ
Without Activity
┌───────────────┐
│ <App> │ ← 全体を一気にハイドレーション
└───────────────┘
With Activity
┌───────────────┐
│ <Activity visible> │ ← 先にattach
│ <Activity hidden> │ ← 後からattach
└───────────────┘結果として、部分的ハイドレーションが可能になります。
注意点
最後に Activity 利用時の注意点をまとめます。
1.非表示中は Effect を止める実装に
mode="hidden" になると 上述のように Effect はクリーンアップされます。そのため、止めたい処理は Effect の返り値(クリーンアップ関数)に書きます。
function Player() {
const ref = useRef();
useEffect(() => {
const videoRef = ref.current;
return () => {
videoRef.pause()
}
}, []);
}2.テキストのみを子にした場合は非表示にならない
子がテキストノードだけの場合、スタイルを適用するDOM要素が無いため mode="hidden" は無効になります。必ず要素やコンポーネントを指定します。
<Activity mode="hidden">
テキストだけ → 隠れない
</Activity>
<Activity mode="hidden"
<span>要素で包めばOK</span>
</Activity>
<Activity mode="hidden"
<UserProfile>
</Activity>3.`<StrictMode>` を使って問題のあるEffectを検出
RootコンポーネントをReactの StrictMode でWrapすると、開発環境で マウント→クリーンアップ→再マウントを意図的に行い、クリーンアップ漏れ(タイマー・購読・動画再生など)を早期に発見することができます。
export default function Root() {
return (
<React.StrictMode>
<App />
</React.StrictMode>
);
}まとめ
以上となります。いかがでしたでしょうか。
この記事で説明した内容を簡単にまとめると以下になります。
<Activity>で、state保持・副作用停止・低優先度プリレンダーを組み合わせてUI/UXを最適化します。- ハイドレーションの境界としても機能し、インタラクティブ化を分割します。
StrictModeで、副作用のクリーンアップを検証して安全に導入できます。
これで今日から Activity を導入する意義を感じてもらえたのではないでしょうか。