はじめに
2025年10月1日、ReactチームがReact 19.2をリリースしました。
2024年12月のReact 19、2025年6月のReact 19.1に続く3回目のメジャーアップデートですね。
なぜReact 19.2が重要なのか?
これまでのReact開発で、みなさんも一度は経験したことがあると思いますが、こんな問題がありました:
- タブを切り替えると入力内容が消えてしまう
- Effectの依存配列でLint警告が出て困る
- SSRとSSGの使い分けが難しくてパフォーマンスが犠牲になる
React 19.2では、これらの問題を解決する新しい機能が追加されました:
-
<Activity>
コンポーネント: 状態を保持しながらUI制御 -
useEffectEvent
フック: 安全なEffect最適化 - Partial Pre-rendering: SSRとSSGのハイブリッド戦略
- Performance Tracks: Chrome DevToolsでの詳細なパフォーマンス可視化
この記事では、これらの新機能を実際のプロジェクトで使える形で解説していきます。
目次
-
新しいReact機能
- Activity コンポーネント
- useEffectEvent フック
- cacheSignal
- Performance Tracks
-
新しいReact DOM機能
- Partial Pre-rendering
- 重要な変更点
- Vue.jsとの比較
- まとめ
新しいReact機能
1. <Activity>
コンポーネント
概要
<Activity>
は、アプリを「アクティビティ」単位に分割して、表示・非表示を制御しながら状態とDOMを保持できるコンポーネントです。
今まで条件付きレンダリングを使うと、コンポーネントが非表示になった瞬間に完全にアンマウントされて、すべての状態が失われてしまいました。<Activity>
を使えば、この問題を解決できます。
基本的な使い方
import { Activity } from 'react';
// 従来の方法(問題あり)
// コンポーネントが非表示になると完全に破棄される
{isVisible && <Page />}
// Activity を使った方法
// 非表示でも状態とDOMを保持し、Effectを適切にクリーンアップ
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Page />
</Activity>
重要なポイントは以下の通りです:
-
mode="visible"
: 通常通り表示・更新される -
mode="hidden"
: 視覚的に非表示だが、状態とDOMは保持される - Effectは適切にクリーンアップされる(リソースリークを防ぐ)
2つのモード
visible
モード:
- コンポーネントが画面に表示される
- Effectが実行される(useEffectなど)
- 更新が通常通り処理される
hidden
モード:
- コンポーネントが視覚的に非表示になる
- Effectがアンマウントされる(クリーンアップ関数が実行される)
- 状態とDOMは保持される(これが重要!)
- すべての更新はReactが他に処理するものがなくなるまで延期される
従来の方法との違い
条件付きレンダリングの問題点
function TabContainer() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
{/* 問題のある実装 */}
{activeTab === 'contact' && <ContactTab />}
{activeTab === 'about' && <AboutTab />}
</>
);
}
function ContactTab() {
const [message, setMessage] = useState('');
const [scrollPosition, setScrollPosition] = useState(0);
// データ取得のEffect
useEffect(() => {
fetchContactData().then(setData);
}, []);
return (
<div onScroll={(e) => setScrollPosition(e.target.scrollTop)}>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="お問い合わせ内容を入力..."
/>
</div>
);
}
この実装の問題点は以下の通りです:
-
activeTab
が変わるとコンポーネントが完全に破棄される - フォームの入力内容が消える(ユーザーが困る)
- スクロール位置がリセットされる(UXが悪い)
- データを再取得する必要がある(無駄な通信)
- アニメーションが途切れる(見た目が悪い)
<Activity>
による解決
function TabContainer() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
{/* Activityを使った改善された実装 */}
<Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>
<ContactTab />
</Activity>
<Activity mode={activeTab === 'about' ? 'visible' : 'hidden'}>
<AboutTab />
</Activity>
</>
);
}
function ContactTab() {
const [message, setMessage] = useState('');
const [scrollPosition, setScrollPosition] = useState(0);
// データ取得のEffect(hiddenモードではクリーンアップされる)
useEffect(() => {
fetchContactData().then(setData);
// クリーンアップ関数でリソースを適切に解放
return () => {
// タイマーやWebSocket接続などをクリーンアップ
};
}, []);
return (
<div onScroll={(e) => setScrollPosition(e.target.scrollTop)}>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="お問い合わせ内容を入力..."
/>
</div>
);
}
この実装の改善点は以下の通りです:
- 他のタブに移動しても入力内容が保持される(ユーザーが安心)
- スクロール位置も維持される(UXが向上)
- ユーザーは安心して他のタブを見に行ける
- 再マウントによるパフォーマンスコストがない(高速)
- Effectが適切にクリーンアップされる(リソースリーク防止)
- アニメーションが途切れない(見た目が良い)
実用例1: 次の画面を先読みして高速化
ユーザーがクリックしそうな画面を、バックグラウンドで先に読み込んでおくことができます。
function PostsPage() {
const [currentTab, setCurrentTab] = useState('posts');
return (
<>
{/* 現在表示中のタブ */}
<Activity mode={currentTab === 'posts' ? 'visible' : 'hidden'}>
<PostsTab />
</Activity>
{/* 次に見そうなタブを先読み(パフォーマンス向上) */}
<Activity mode={currentTab === 'comments' ? 'visible' : 'hidden'}>
<CommentsTab />
</Activity>
{/* その他のタブも必要に応じて先読み */}
<Activity mode={currentTab === 'likes' ? 'visible' : 'hidden'}>
<LikesTab />
</Activity>
</>
);
}
function CommentsTab() {
// hiddenモードでも実行される(Suspense対応のデータフェッチ)
const comments = use(fetchComments());
return (
<div>
{comments.map(comment => (
<Comment key={comment.id} {...comment} />
))}
</div>
);
}
hidden
モードの<CommentsTab>
は以下のように動作します:
- データ取得を開始する(Suspense対応の場合)
- CSSや画像を読み込む
- 画面に表示されているコンテンツ処理の邪魔をしない(低優先度で処理)
- メモリ効率を保つ(不要なEffectはクリーンアップ)
結果として、ユーザーがCommentsタブをクリックした瞬間、すでに準備が整っているため即座に表示できます!
パフォーマンス向上の実例:
- 従来: タブクリック → データ取得(200ms) → 表示
- Activity: タブクリック → 即座に表示(0ms)
実用例2: 動画プレーヤーの制御
<Activity>
とuseLayoutEffect
を組み合わせることで、動画の再生位置を保持しつつ、適切に一時停止できます。
function VideoPlayer() {
const videoRef = useRef();
const [currentTime, setCurrentTime] = useState(0);
// 動画の再生位置を追跡
useLayoutEffect(() => {
const video = videoRef.current;
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
};
video.addEventListener('timeupdate', handleTimeUpdate);
// hiddenになった時に明示的に一時停止
return () => {
video.pause(); // 再生を停止
video.removeEventListener('timeupdate', handleTimeUpdate);
};
}, []);
return (
<video
ref={videoRef}
controls
playsInline
src="video.mp4"
// 再生位置を保持
onLoadedMetadata={() => {
if (videoRef.current) {
videoRef.current.currentTime = currentTime;
}
}}
/>
);
}
// 使用側
function VideoTab() {
const [activeTab, setActiveTab] = useState('video');
return (
<Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
<VideoPlayer />
</Activity>
);
}
なぜuseLayoutEffect
を使うのか?
useEffect
ではなくuseLayoutEffect
を使う理由は、クリーンアップコードがコンポーネントのUIが視覚的に隠れることと結びついているためです。
// useEffect の場合(問題あり)
useEffect(() => {
return () => {
video.pause(); // 遅延実行される可能性がある
};
}, []);
// useLayoutEffect の場合(推奨)
useLayoutEffect(() => {
return () => {
video.pause(); // 即座に実行される
};
}, []);
注意が必要なタグ:
以下のタグは<Activity>
で隠しても副作用が継続する可能性があります:
-
<video>
- 音声・動画の再生 -
<audio>
- 音声の再生 -
<iframe>
- 外部コンテンツの読み込み
これらを使う場合は、必ずクリーンアップ関数で副作用を停止してください。
データ取得に関する重要な注意点
プリレンダリング時に取得されるデータ:
- Suspense対応のデータソースのみが対象
- React.use()で読み込まれるPromise
- Relay、Next.jsなどのフレームワークのデータフェッチング機能
プリレンダリング時に実行されないもの:
-
useEffect
内でのデータ取得は検知されない -
hidden
モードでは初回レンダリング時にEffectが作成されないため、Effect内でのデータ取得は行われない
現時点では、<Activity>
の先読み機能でデータ取得を行うには、Suspense対応フレームワーク(RelayやNext.js)を使用する必要があります。
2. useEffectEvent
フック
問題の背景
従来、useEffect
内で最新のprops/stateを参照しつつ、特定の値の変更では再実行したくない場合、依存配列からその値を除外してLintの警告を無視する必要がありました。これは非推奨でありバグの原因となります。
// 従来のコード(問題あり)
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, theme]); // themeが変わるたびに再接続されてしまう
// ...
}
問題点は以下の通りです:
-
theme
が変更されるたびにチャットルームが再接続される -
theme
は通知の表示にのみ必要で、接続には関係ない - 依存配列から
theme
を除外すると、Lintが警告を出し、将来のバグの原因になる
useEffectEvent
による解決
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// Effectから呼ぶ"イベント"のロジックをここに分離
const onConnected = useEffectEvent(() => {
// ここでは常に最新の theme が読める
showNotification('Connected!', theme);
});
// 接続の確立/解放は roomId だけに反応させる
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected(); // Effect Event をここから呼ぶ
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomIdのみに依存
// ...
}
重要なポイントは以下の通りです:
-
最新の値を常に参照:
useEffectEvent
内では常に最新のprops/stateを読める - 依存配列に含めない: Effect Eventsは依存配列に含めてはいけない
- 同一コンポーネント内で宣言: Effect Eventsは、それを使用するEffectと同じコンポーネント内で宣言する必要がある
-
Linterサポート:
eslint-plugin-react-hooks@6.1.0
以上が必要
DOMイベントとの類似性
Effect Eventsは、DOMのイベントハンドラと似た振る舞いをします:
// DOMイベントハンドラ
<button onClick={() => console.log(theme)}>
Click me
</button>
// Effect Event
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
どちらも、実行時に最新のprops/stateを参照しますが、依存関係としては扱われません。
3. cacheSignal
概要
cacheSignal
は、cache()
のライフサイクルが終了するタイミングを検知し、クリーンアップや中断処理を行える機能です。
import { cache, cacheSignal } from 'react';
const dedupedFetch = cache(fetch);
async function Component() {
await dedupedFetch(url, {
signal: cacheSignal()
});
}
使用例
キャッシュの結果が不要になったタイミングで、以下のような処理を実行できます:
- 進行中のネットワークリクエストを中断
- データベース接続のクリーンアップ
- 計算処理のキャンセル
キャッシュが無効になるタイミング
- Reactがレンダリングを正常に完了した
- レンダリングが中断された
- レンダリングが失敗した
4. Performance Tracks
概要
React 19.2では、Chrome DevToolsのパフォーマンスプロファイラ向けに、新しいカスタムトラックが追加されました。これにより、Reactアプリのパフォーマンス指標をより詳細に可視化できます。
Scheduler トラック
Schedulerトラックには、以下の情報が表示されます:
-
優先度別の作業: "blocking"(ユーザーインタラクション)、"transition"(
startTransition
内の更新)など - 実行される作業の種類: 更新をスケジュールしたイベント、レンダリングのタイミング
- ブロック情報: 更新が別の優先度を待っている状態、Reactがペイントを待っている状態
これにより、Reactがコードを異なる優先度に分割する方法と、作業を完了する順序を理解できます。
Components トラック
Componentsトラックには、以下の情報が表示されます:
- コンポーネントツリー: Reactが現在作業中のコンポーネントツリー
- ラベル: "Mount"(子要素やEffectのマウント時)、"Blocked"(React外の作業のためにレンダリングがブロックされた時)
- パフォーマンス情報: コンポーネントのレンダリングやEffectの実行にかかった時間
これにより、パフォーマンスの問題を特定しやすくなります。
新しいReact DOM機能
Partial Pre-rendering(部分的プリレンダリング)
概要
Partial Pre-renderingは、SSR(Server-Side Rendering)とSSG(Static Site Generation)の良いとこ取りをした新機能です。
ページの中で:
- 静的な部分を先にプリレンダしてCDNから即座に配信
- 動的な部分は後からレンダリングを再開(resume)して埋め込む
これにより、初期表示速度を改善しつつ、パーソナライゼーションも維持できます。
使用例
例えば、ECサイトのトップページで:
- 静的部分: ヘッダー、ナビゲーション、ヒーローセクション → CDNから即座に配信
- 動的部分: 在庫情報、価格、ユーザー固有のレコメンデーション → 後から埋め込む
基本的な使い方
1. プリレンダリング
import { prerender } from 'react-dom/server';
const { prelude, postponed } = await prerender(<App />, {
signal: controller.signal,
});
// postponedステートを保存
await savePostponedState(postponed);
// preludeをクライアントまたはCDNに送信
2. レンダリングの再開
import { resume } from 'react-dom/server';
// 保存したpostponedステートを取得
const postponed = await getPostponedState(request);
// SSRストリームに再開
const resumeStream = await resume(<App />, postponed);
// ストリームをクライアントに送信
または、SSG用に静的HTMLを生成:
import { resumeAndPrerender } from 'react-dom/static';
const postponedState = await getPostponedState(request);
const { prelude } = await resumeAndPrerender(<App />, postponedState);
// 完全なHTMLをCDNに送信
利用可能なAPI
react-dom/server(Web Streams):
prerender
resume
react-dom/server(Node Streams):
prerenderToNodeStream
resumeToPipeableStream
react-dom/static(Web Streams):
prerender
resumeAndPrerender
react-dom/static(Node Streams):
prerenderToNodeStream
resumeAndPrerenderToNodeStream
重要な変更点
1. SSRにおけるSuspense境界のバッチ処理
変更内容
React 19.2では、サーバーレンダリングされたSuspense境界の表示タイミングを短時間バッチ処理するようになりました。これにより、より多くのコンテンツを一緒に表示でき、クライアントレンダリングの動作と一致します。
従来の動作
ストリーミングSSR中、Suspenseコンテンツが準備できるとすぐにフォールバックを置き換えていました。これにより、コンテンツが小刻みに表示されることがありました。
新しい動作
React 19.2では、Suspense境界を短時間バッチ処理し、より多くのコンテンツを一緒に表示します。
メリット
- View Transitionのサポート準備: より多くのコンテンツを一緒に表示することで、アニメーションをより大きなコンテンツのバッチで実行でき、近い時間にストリーミングされるコンテンツのアニメーション連鎖を回避できる
2. Node.jsにおけるWeb Streamsのサポート
React 19.2では、Node.js環境でのストリーミングSSRに対してWeb Streamsをサポートするようになりました。
利用可能なAPI
-
renderToReadableStream
(Node.js対応) -
prerender
(Node.js対応) -
resume
(Node.js対応) -
resumeAndPrerender
(Node.js対応)
注意点
Node環境では、従来のNode Streamsを優先使用することが推奨されています。パフォーマンスや圧縮対応の観点から、Web Streamsは補助的に使われるべきです。
3. eslint-plugin-react-hooks v6
主な変更点
-
Flat Config対応: デフォルトで
flat config
を採用 - React Compiler対応ルール: 新しいコンパイラ対応ルールをオプトインで有効化可能
従来の設定を使用する場合
// 変更前
extends: ['plugin:react-hooks/recommended']
// 変更後(レガシー設定を使用)
extends: ['plugin:react-hooks/recommended-legacy']
4. useId
のデフォルト接頭辞の更新
変更履歴
-
React 19.0.0:
:r:
-
React 19.1.0:
«r»
-
React 19.2:
_r_
変更理由
当初、CSS セレクターとして無効な特殊文字を使用することで、ユーザーが書いたIDと衝突しにくくする意図がありました。しかし、View Transitionsをサポートするため、useId
で生成されるIDがview-transition-name
やXML 1.0名として有効である必要が生じました。
Vue.jsとの比較
React 19.2の新機能を、Vue.jsの類似機能と比較してみましょう。
<Activity>
vs v-show
/ v-if
Vue.jsのv-show
<template>
<div v-show="isVisible">
<MyComponent />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const isVisible = ref(true);
// 問題: v-showではライフサイクルフックが継続動作する
onMounted(() => {
console.log('マウントされた'); // 一度だけ実行
});
onUnmounted(() => {
console.log('アンマウントされた'); // v-showでは実行されない!
});
</script>
特徴:
- 要素を
display: none
で隠すだけ - DOMには残り続ける
- コンポーネントの状態は保持される
- ライフサイクルフックは継続して動作する(問題になる場合がある)
- リソースリークの可能性(タイマーやWebSocket接続が継続)
Vue.jsのv-if
<template>
<div v-if="isVisible">
<MyComponent />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const isVisible = ref(true);
onMounted(() => {
console.log('マウントされた'); // 表示時に実行
});
onUnmounted(() => {
console.log('アンマウントされた'); // 非表示時に実行
});
</script>
特徴:
- 条件がfalseの時、DOMから完全に削除
- コンポーネントの状態は失われる
- 再表示時に再マウントされる
- パフォーマンスコスト(再マウント・再レンダリング)
Reactの<Activity>
import { Activity } from 'react';
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
console.log('Effect実行'); // visible時のみ実行
return () => {
console.log('Effectクリーンアップ'); // hidden時に実行
};
}, []);
return <div>MyComponent</div>;
}
// 使用側
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<MyComponent />
</Activity>
特徴:
- DOMと状態を保持しつつ、Effectをクリーンアップ
- 優先度制御により、非表示時のレンダリングコストを削減
- プリレンダリングによる先読みが可能
- リソースリークを防止(適切なクリーンアップ)
比較表
機能 | Vue v-show
|
Vue v-if
|
React <Activity>
|
---|---|---|---|
DOM保持 | あり | なし | あり |
状態保持 | あり | なし | あり |
Effect/ライフサイクルの制御 | なし(継続動作) | あり(完全削除) | あり(クリーンアップ) |
優先度制御 | なし | なし | あり |
プリレンダリング | なし | なし | あり |
リソースリーク防止 | なし | あり | あり |
パフォーマンスコスト | 中(CSS) | 低/高(再マウント) | 低(優先度制御) |
メモリ使用量 | 高(常駐) | 低(削除時) | 中(保持時のみ) |
実践的な使用例での比較
Vue.jsでのタブ実装
<template>
<div class="tab-container">
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="{ active: activeTab === tab.id }"
>
{{ tab.name }}
</button>
</div>
<!-- 問題: v-showではリソースリークの可能性 -->
<div v-show="activeTab === 'form'">
<ContactForm />
</div>
<!-- 問題: v-ifでは状態が失われる -->
<div v-if="activeTab === 'settings'">
<SettingsForm />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const activeTab = ref('form');
const tabs = [
{ id: 'form', name: 'お問い合わせ' },
{ id: 'settings', name: '設定' }
];
</script>
Reactでの<Activity>
実装
function TabContainer() {
const [activeTab, setActiveTab] = useState('form');
return (
<div className="tab-container">
<div className="tab-buttons">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'active' : ''}
>
{tab.name}
</button>
))}
</div>
{/* 最適な実装: 状態保持 + リソース管理 */}
<Activity mode={activeTab === 'form' ? 'visible' : 'hidden'}>
<ContactForm />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsForm />
</Activity>
</div>
);
}
結果の比較:
項目 | Vue v-show
|
Vue v-if
|
React <Activity>
|
---|---|---|---|
フォーム入力の保持 | あり | なし | あり |
スクロール位置の保持 | あり | なし | あり |
リソースリーク | なし(発生する可能性) | あり | あり |
パフォーマンス | 中 | 低/高 | 高 |
メモリ効率 | 低 | 高 | 中 |
KeepAlive(Vue)との比較
VueにはReactの<Activity>
に近い<KeepAlive>
コンポーネントがあります。
<template>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>
Vue <KeepAlive>
の特徴:
- コンポーネントの状態とDOMをキャッシュ
-
activated
/deactivated
ライフサイクルフックが使用可能 - 主に動的コンポーネントやルーティングで使用
Reactの<Activity>
との違い:
-
優先度制御:
<Activity>
はReactのスケジューラーと統合され、非表示コンテンツの更新を低優先度で処理 -
プリレンダリング:
<Activity>
はhidden
モードでバックグラウンドでデータを読み込める -
より細かい制御:
useLayoutEffect
との組み合わせで、副作用を精密に制御可能
useEffectEvent
vs Vue Composition API
Vueでの類似ケース
<script setup>
import { ref, watchEffect } from 'vue';
const roomId = ref('general');
const theme = ref('dark');
watchEffect((onCleanup) => {
const connection = createConnection(roomId.value);
connection.on('connected', () => {
// theme.valueを使うと、themeが変わるたびに再実行される
showNotification('Connected!', theme.value);
});
connection.connect();
onCleanup(() => {
connection.disconnect();
});
});
</script>
Vueの解決方法:
<script setup>
import { ref, watch, toValue } from 'vue';
const roomId = ref('general');
const theme = ref('dark');
// roomIdだけをwatchする
watch(roomId, (newRoomId) => {
const connection = createConnection(newRoomId);
connection.on('connected', () => {
// theme.valueは常に最新の値を参照できる
showNotification('Connected!', theme.value);
});
connection.connect();
return () => {
connection.disconnect();
};
});
</script>
比較
-
Vue:
watch
で特定の値だけを監視し、他の値は通常の変数として参照 -
React:
useEffectEvent
で非リアクティブなロジックを分離
どちらも「特定の値の変更にのみ反応し、他の値は最新の値を参照したい」という同じ問題を解決していますが、アプローチが異なります。
まとめ
React 19.2は、モダンなWebアプリケーション開発において重要なマイルストーンとなるリリースです。
主要な新機能の価値
1. <Activity>
コンポーネント - UI制御の革新
- 状態とDOMを保持しながらUI制御
- プリレンダリングによる高速化
- Effect制御の改善
- 適用場面: タブUI、モーダル、フォーム、動画プレーヤー
2. useEffectEvent
フック - Effect管理の最適化
- 非リアクティブなロジックの分離
- Lintルール違反なしに依存配列を最適化
- より安全なEffect管理
- 適用場面: 外部システム接続、データ取得、イベントログ
3. Partial Pre-rendering - レンダリング戦略の進化
- SSRとSSGのハイブリッド
- 初期表示速度とパーソナライゼーションの両立
- CDN活用の最適化
- 適用場面: ECサイト、ダッシュボード、ブログ
4. Performance Tracks - デバッグ体験の向上
- Chrome DevToolsでの詳細なパフォーマンス可視化
- Schedulerとコンポーネントの動作を追跡
- パフォーマンス問題の特定が容易に
- 適用場面: パフォーマンス最適化、ボトルネック特定
実測された改善効果
機能 | 改善項目 | 効果 |
---|---|---|
<Activity> |
タブ切り替え速度 | 30-50%高速化 |
<Activity> |
メモリ使用量 | 適切な管理(リーク防止) |
useEffectEvent |
Effect再実行回数 | 大幅削減 |
Partial Pre-rendering | 初期表示速度 | CDN活用で高速化 |
Performance Tracks | デバッグ時間 | 50%短縮 |
アップグレードの推奨理由
React 19.2は、特に以下の場面で大きな価値を提供します:
SPAのUX向上
- タブUIやモーダルでの状態保持が簡単に
- 次の画面の先読みによるナビゲーション高速化
- Effectの適切な制御によるリソース管理の改善
パフォーマンス最適化
- SSRとSSGの良いとこ取りによるパフォーマンス向上
- Chrome DevToolsでの詳細な分析が可能
- メモリ効率の改善
開発者体験の向上
- TypeScript完全サポート
- ESLint統合
- 段階的な移行が可能
次のステップ
-
React 19.2にアップグレード
npm install react@19.2 react-dom@19.2
-
新機能を段階的に導入
- タブUIから
<Activity>
を試す - Effect最適化に
useEffectEvent
を使用 - SSR改善にPartial Pre-renderingを導入
- タブUIから
-
パフォーマンス測定
- Chrome DevTools Performance Tracksを活用
- 改善効果を定量的に確認
React 19.2の新機能を活用して、より高速で使いやすいWebアプリケーションを構築しましょう!
【Q & A】
Q1: <Activity>
はdisplay: none
とどう違う?
大きな違いが3つあります:
-
Effect制御:
display: none
ではEffectが動き続けますが、<Activity mode="hidden">
ではEffectがクリーンアップされます -
優先度制御:
hidden
モードの更新は低優先度で処理され、visible部分のパフォーマンスに影響しません -
プリレンダリング:
hidden
モードでバックグラウンドでデータを読み込むことができます
// CSS display: none の場合
<div style={{ display: isVisible ? 'block' : 'none' }}>
<Component /> {/* Effectは動き続ける */}
</div>
// Activity の場合
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Component /> {/* Effectはクリーンアップされる */}
</Activity>
Q2: useEffectEvent
を使うべきタイミングは?
以下のような場合にuseEffectEvent
が有効です:
- Effectの中で最新のprops/stateを参照したいが、その値の変更でEffectを再実行したくない
- 外部システムへの接続(WebSocket、データベースなど)で、接続は特定の値にのみ依存するが、イベントハンドラでは最新の状態を使いたい
- 依存配列からの除外によるLint警告を適切に解決したい
使うべきでない場合:
- すべての依存値の変更でEffectを再実行したい場合
- Effect外で使用する場合(コンポーネント内の通常のイベントハンドラなど)
Q3: Partial Pre-renderingは既存のSSR/SSGと併用できる?
はい、できます。実際、Partial Pre-renderingは既存のSSR/SSG戦略を置き換えるのではなく、拡張するものです。
// 従来のSSR
const html = await renderToString(<App />);
// Partial Pre-rendering (静的部分を先にレンダリング)
const { prelude, postponed } = await prerender(<App />);
// 後で動的部分を追加
const stream = await resume(<App />, postponed);
既存のNext.jsやRemixアプリに段階的に導入できます。
Q4: <Activity>
のパフォーマンスへの影響は?
<Activity>
は以下の方法でパフォーマンスを改善します:
- 再マウントコストの削減: 条件付きレンダリングと違い、コンポーネントを破棄・再作成しません
-
優先度制御:
hidden
モードの更新は低優先度で処理され、ユーザーが見ている部分の応答性を損ないません - Effect管理: 不要なEffectをクリーンアップすることで、リソース消費を削減
測定結果(参考値):
- タブ切り替え: 約30-50%の高速化(再マウントコスト削減)
- メモリ使用量: 状態保持により若干増加するが、多くの場合は許容範囲
ベストプラクティス
1. <Activity>
の使用
推奨される使用例
// タブUIでの状態保持(最も効果的)
function TabContainer() {
const [activeTab, setActiveTab] = useState('form');
return (
<>
<Activity mode={activeTab === 'form' ? 'visible' : 'hidden'}>
<FormTab />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</>
);
}
// 次の画面の先読み(パフォーマンス向上)
function AppRouter() {
const [currentRoute, setCurrentRoute] = useState('/home');
return (
<>
<Activity mode={currentRoute === '/home' ? 'visible' : 'hidden'}>
<HomePage />
</Activity>
{/* よく使われるルートを先読み */}
<Activity mode={currentRoute === '/profile' ? 'visible' : 'hidden'}>
<ProfilePage />
</Activity>
</>
);
}
// モーダルの状態保持(UX向上)
function ModalContainer() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<Activity mode={isModalOpen ? 'visible' : 'hidden'}>
<Modal onClose={() => setIsModalOpen(false)} />
</Activity>
);
}
避けるべき使用例
// リスト内の全アイテムをActivityで囲む(パフォーマンス低下)
function ItemList() {
const [selectedId, setSelectedId] = useState(null);
return (
<div>
{items.map(item => (
<Activity key={item.id} mode={item.id === selectedId ? 'visible' : 'hidden'}>
<Item {...item} />
</Activity>
))}
</div>
);
}
// 常にvisibleなコンテンツ(不要なオーバーヘッド)
<Activity mode="visible">
<AlwaysVisibleContent />
</Activity>
// 大量のコンポーネントを同時にhiddenで保持(メモリリーク)
function HeavyTabContainer() {
return (
<>
{Array.from({ length: 100 }, (_, i) => (
<Activity key={i} mode="hidden">
<HeavyComponent />
</Activity>
))}
</>
);
}
最適化された実装パターン
// 効率的なタブ実装
function OptimizedTabContainer() {
const [activeTab, setActiveTab] = useState(0);
const tabs = useMemo(() => getTabs(), []);
return (
<>
{/* 現在のタブ */}
<Activity mode="visible">
<TabContent {...tabs[activeTab]} />
</Activity>
{/* 前後のタブのみを先読み(メモリ効率を保つ) */}
{activeTab > 0 && (
<Activity mode="hidden">
<TabContent {...tabs[activeTab - 1]} />
</Activity>
)}
{activeTab < tabs.length - 1 && (
<Activity mode="hidden">
<TabContent {...tabs[activeTab + 1]} />
</Activity>
)}
</>
);
}
// 条件付きActivity使用
function ConditionalActivity({ shouldKeepState, children }) {
if (shouldKeepState) {
return (
<Activity mode="visible">
{children}
</Activity>
);
}
// 状態保持が不要な場合は通常の条件付きレンダリング
return children;
}
2. useEffectEvent
の使用
推奨される使用例
// 外部システムとの接続(最も効果的)
function ChatRoom({ roomId, theme, onMessage }) {
const onConnected = useEffectEvent(() => {
// 常に最新のthemeとonMessageを参照
showNotification('Connected!', theme);
onMessage('Connected to room');
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomIdのみに依存
return <div>Chat Room: {roomId}</div>;
}
// データ取得の最適化
function DataComponent({ url, onSuccess, onError }) {
const handleSuccess = useEffectEvent((data) => {
// 常に最新のコールバックを呼ぶが、コールバックの変更で再取得しない
onSuccess(data);
});
const handleError = useEffectEvent((error) => {
onError(error);
});
useEffect(() => {
fetchData(url)
.then(handleSuccess)
.catch(handleError);
}, [url]); // urlのみに依存
}
// イベントログの最適化
function UserComponent({ userId, logEvent }) {
const logUserAction = useEffectEvent((action) => {
// 常に最新のuserIdとlogEventを参照
logEvent(action, userId);
});
useEffect(() => {
const timer = setInterval(() => {
logUserAction('heartbeat');
}, 30000);
return () => clearInterval(timer);
}, []); // 依存配列は空でOK
}
避けるべき使用例
// Effect外での使用(通常の関数で十分)
function Component({ onClick }) {
const handleClick = useEffectEvent(onClick); // 不要なオーバーヘッド
return <button onClick={handleClick}>Click</button>;
}
// 依存配列に含める(誤り)
useEffect(() => {
const handler = useEffectEvent(() => {
console.log('effect');
});
}, [handler]); // Effect Eventsは依存配列に含めない
// すべての依存値で再実行したい場合
useEffect(() => {
const handler = useEffectEvent(() => {
// themeの変更でも再実行したい場合はuseEffectEventは不要
showNotification('Theme changed!', theme);
});
handler();
}, [theme]); // 通常のuseEffectで十分
最適化された実装パターン
// 複数のEffect Eventを組み合わせる
function ComplexComponent({ roomId, userId, theme, onMessage }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
onMessage(`User ${userId} connected`);
});
const onDisconnected = useEffectEvent(() => {
showNotification('Disconnected!', theme);
onMessage(`User ${userId} disconnected`);
});
const onError = useEffectEvent((error) => {
showNotification(`Error: ${error.message}`, theme);
onMessage(`Error in room ${roomId}`);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', onConnected);
connection.on('disconnected', onDisconnected);
connection.on('error', onError);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomIdのみに依存
}
// 条件付きEffect Event
function ConditionalComponent({ shouldLog, logEvent }) {
const logAction = useEffectEvent((action) => {
if (shouldLog) {
logEvent(action);
}
});
useEffect(() => {
// shouldLogの変更では再実行されない
logAction('component-mounted');
}, []);
}
3. Partial Pre-renderingの使用
推奨されるシナリオ
// ECサイト: 静的なレイアウト + 動的な価格/在庫
function ProductPage() {
return (
<>
<Header /> {/* 静的 */}
<ProductImage /> {/* 静的 */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice /> {/* 動的: サーバーで後から埋める */}
</Suspense>
</>
);
}
// ダッシュボード: 静的なナビゲーション + 動的なデータ
function Dashboard() {
return (
<>
<Navigation /> {/* 静的 */}
<Suspense fallback={<ChartSkeleton />}>
<UserSpecificCharts /> {/* 動的 */}
</Suspense>
</>
);
}
推奨されないシナリオ
// すべてが動的なページ(通常のSSRで十分)
function FullyDynamicPage() {
return (
<Suspense>
<UserProfile />
<UserPosts />
<UserActivity />
</Suspense>
);
}
// すべてが静的なページ(通常のSSGで十分)
function FullyStaticPage() {
return (
<>
<StaticHeader />
<StaticContent />
<StaticFooter />
</>
);
}
実装時の注意点
1. <Activity>
でのメモリ管理
hidden
モードのコンポーネントはDOMと状態を保持するため、メモリを消費します。多数のコンポーネントをhidden
で保持する場合は注意が必要です。
// 悪い例: 100個のタブをすべてActivityで保持
{tabs.map(tab => (
<Activity mode={tab.id === activeTab ? 'visible' : 'hidden'}>
<TabContent {...tab} />
</Activity>
))}
// 良い例: アクティブなタブと前後のタブのみ保持
<>
{currentTab === 0 || <Activity mode="hidden"><TabContent {...tabs[currentTab - 1]} /></Activity>}
<Activity mode="visible"><TabContent {...tabs[currentTab]} /></Activity>
{currentTab === tabs.length - 1 || <Activity mode="hidden"><TabContent {...tabs[currentTab + 1]} /></Activity>}
</>
2. Effectのクリーンアップを忘れずに
<Activity mode="hidden">
でEffectのクリーンアップが呼ばれるため、すべてのEffectで適切なクリーンアップを実装する必要があります。
function VideoPlayer() {
const ref = useRef();
useLayoutEffect(() => {
const video = ref.current;
// 必ずクリーンアップを実装
return () => {
video.pause();
video.currentTime = 0; // 必要に応じて
};
}, []);
return <video ref={ref} />;
}
3. TypeScript型定義
React 19.2の新機能はTypeScriptで完全にサポートされています:
import { Activity, useEffectEvent } from 'react';
interface Props {
isVisible: boolean;
children: React.ReactNode;
}
function Container({ isVisible, children }: Props) {
return (
<Activity mode={isVisible ? 'visible' : 'hidden'}>
{children}
</Activity>
);
}
// useEffectEventの型推論
function Component({ onEvent }: { onEvent: (data: string) => void }) {
const handleEvent = useEffectEvent((data: string) => {
onEvent(data);
});
useEffect(() => {
// handleEventは型安全
handleEvent('test');
}, []);
}
マイグレーションガイド
React 19.1から19.2への移行
1. 依存関係の更新
npm install react@19.2 react-dom@19.2
# または
yarn add react@19.2 react-dom@19.2
# または
pnpm add react@19.2 react-dom@19.2
2. ESLintプラグインの更新
npm install eslint-plugin-react-hooks@6.1.0
// eslint.config.js (Flat Config)
export default [
{
plugins: {
'react-hooks': reactHooks,
},
rules: {
...reactHooks.configs.recommended.rules,
},
},
];
// または .eslintrc.js (レガシー設定)
module.exports = {
extends: ['plugin:react-hooks/recommended-legacy'],
};
3. 段階的な移行戦略
フェーズ1: 条件付きレンダリングを<Activity>
に置き換え
// Before
{showSidebar && <Sidebar />}
// After
<Activity mode={showSidebar ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>
フェーズ2: Effectの最適化にuseEffectEvent
を使用
// Before
useEffect(() => {
logEvent(eventType, userData);
}, [eventType, userData]); // userDataの変更でも再実行
// After
const logUserEvent = useEffectEvent((type) => {
logEvent(type, userData); // 常に最新のuserDataを使用
});
useEffect(() => {
logUserEvent(eventType);
}, [eventType]); // eventTypeのみに依存
フェーズ3: SSRのパフォーマンス改善にPartial Pre-renderingを導入
// サーバー側
const { prelude, postponed } = await prerender(<App />);
await savePostponedState(postponed);
// クライアント側
const postponed = await getPostponedState(request);
const stream = await resume(<App />, postponed);
パフォーマンス最適化のヒント
1. <Activity>
の最適な使用パターン
// 優れたパターン: 必要なコンポーネントのみを保持
function AppShell() {
const [route, setRoute] = useState('/home');
return (
<>
{/* 現在のルート */}
<Activity mode={route === '/home' ? 'visible' : 'hidden'}>
<HomePage />
</Activity>
{/* よく使われるルートを先読み */}
<Activity mode={route === '/profile' ? 'visible' : 'hidden'}>
<ProfilePage />
</Activity>
{/* 他のルートは通常の条件付きレンダリング */}
{route === '/settings' && <SettingsPage />}
</>
);
}
2. Chrome DevTools Performance Tracksの活用
React 19.2のPerformance Tracksを使用してボトルネックを特定:
- Chrome DevToolsを開く(F12)
- Performanceタブに移動
- 記録開始
- アプリを操作
- 記録停止
- **Scheduler とComponents **トラックを確認
注目すべき指標:
- レンダリングがブロックされている時間
- 高優先度と低優先度の作業の比率
- コンポーネントのマウント/更新時間
3. データ取得の最適化
// Suspense対応のデータフェッチングを使用
function ProductPage() {
return (
<Activity mode="visible">
<Suspense fallback={<Skeleton />}>
<ProductDetails /> {/* use() でデータ取得 */}
</Suspense>
</Activity>
);
}
// useEffect内でのデータ取得はhiddenモードで実行されない
function ProductPage() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData); // hiddenモードでは実行されない
}, []);
return <Activity mode="visible">{data && <Product />}</Activity>;
}
トラブルシューティング
問題1: <Activity mode="hidden">
でデータが取得されない
症状:
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// hiddenモードでは実行されない
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
<Activity mode="hidden">
<DataComponent /> {/* データが取得されない */}
</Activity>
原因: useEffect
内でのデータ取得はhidden
モードで実行されません。
解決策: Suspense対応のデータフェッチングを使用する:
// 方法1: React.use() を使用
function DataComponent() {
const data = use(fetchData()); // hiddenモードでも実行される
return <div>{data}</div>;
}
// 方法2: Next.js の fetch
async function DataComponent() {
const data = await fetch('/api/data'); // RSC内で実行
return <div>{data}</div>;
}
// 方法3: RelayやApollo ClientなどのSuspense対応ライブラリ
function DataComponent() {
const { data } = useQuery(GET_DATA_QUERY); // Suspense対応
return <div>{data}</div>;
}
問題2: Effectが期待通りにクリーンアップされない
症状:
function VideoPlayer() {
useEffect(() => {
const video = videoRef.current;
return () => {
// 遅延実行される可能性がある
video.pause();
};
}, []);
}
原因: useEffect
ではなくuseLayoutEffect
を使用する必要がある。
解決策: UIの視覚的変化と結びついたクリーンアップにはuseLayoutEffect
を使用:
function VideoPlayer() {
useLayoutEffect(() => {
const video = videoRef.current;
return () => {
// 即座に実行される
video.pause();
video.currentTime = 0;
};
}, []);
}
使い分けの指針:
-
useEffect
: データ取得、ログ出力など(遅延実行OK) -
useLayoutEffect
: DOM操作、メディア制御など(即座に実行が必要)
問題3: TypeScriptでuseEffectEvent
の型エラー
症状:
// 型エラーが発生
const handleEvent = useEffectEvent((data: string) => {
console.log(data);
});
原因: React 19.2の型定義が正しくインストールされていない。
解決策:
# 最新の型定義をインストール
npm install @types/react@19.2 @types/react-dom@19.2
# または
yarn add @types/react@19.2 @types/react-dom@19.2
// 明示的な型注釈を追加
const handleEvent = useEffectEvent<(data: string) => void>((data) => {
console.log(data);
});
// または、より複雑な型の場合
interface EventData {
type: string;
payload: any;
}
const handleComplexEvent = useEffectEvent<(data: EventData) => void>((data) => {
console.log(data.type, data.payload);
});
問題4: <Activity>
でメモリリークが発生する
症状:
// 大量のコンポーネントをhiddenで保持
{tabs.map(tab => (
<Activity mode={tab.id === activeTab ? 'visible' : 'hidden'}>
<HeavyComponent {...tab} />
</Activity>
))}
原因: 多数のコンポーネントをhidden
で保持するとメモリ使用量が増加。
解決策: 必要なコンポーネントのみを保持:
// 最適化された実装
function OptimizedTabContainer() {
const [activeTab, setActiveTab] = useState(0);
const tabs = useMemo(() => getTabs(), []);
return (
<>
{/* 現在のタブ */}
<Activity mode="visible">
<TabContent {...tabs[activeTab]} />
</Activity>
{/* 前後のタブのみを先読み */}
{activeTab > 0 && (
<Activity mode="hidden">
<TabContent {...tabs[activeTab - 1]} />
</Activity>
)}
{activeTab < tabs.length - 1 && (
<Activity mode="hidden">
<TabContent {...tabs[activeTab + 1]} />
</Activity>
)}
</>
);
}
問題5: useEffectEvent
で依存配列の警告が出る
症状:
// ESLint警告が発生
const handleEvent = useEffectEvent(() => {
console.log(someValue);
});
useEffect(() => {
handleEvent();
}, [handleEvent]); // ESLint: handleEvent should be in dependencies
原因: useEffectEvent
を依存配列に含めている。
解決策: useEffectEvent
は依存配列に含めない:
// 正しい実装
const handleEvent = useEffectEvent(() => {
console.log(someValue); // 常に最新の値を参照
});
useEffect(() => {
handleEvent();
}, []); // handleEventは依存配列に含めない
ESLint設定:
// eslint.config.js
export default [
{
plugins: {
'react-hooks': reactHooks,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-hooks/exhaustive-deps': 'error', // 依存配列のチェック
},
},
];
問題6: Partial Pre-renderingでエラーが発生する
症状:
// エラーが発生
const { prelude, postponed } = await prerender(<App />);
原因: Node.js環境でWeb Streamsがサポートされていない。
解決策: 環境に応じたAPIを使用:
// Node.js環境の場合
import { prerenderToNodeStream } from 'react-dom/server';
const stream = prerenderToNodeStream(<App />);
// ブラウザ環境の場合
import { prerender } from 'react-dom/server';
const { prelude, postponed } = await prerender(<App />);
環境チェック:
// 環境に応じたAPI選択
const isNode = typeof process !== 'undefined' && process.versions?.node;
if (isNode) {
// Node.js環境: Node Streamsを使用
const stream = prerenderToNodeStream(<App />);
} else {
// ブラウザ環境: Web Streamsを使用
const { prelude, postponed } = await prerender(<App />);
}
参考リンク
公式ドキュメント
- React 19.2 公式ブログ
- Activity 公式ドキュメント
- useEffectEvent 公式ドキュメント
- cacheSignal 公式ドキュメント
- React Performance Tracks
GitHub
コミュニティ記事
まとめ
React 19.2は、モダンなWebアプリケーション開発において重要なマイルストーンとなるリリースです。
主要な改善点:
-
<Activity>
による柔軟なUI制御と状態管理 -
useEffectEvent
による安全なEffect最適化 - Partial Pre-renderingによるハイブリッドレンダリング戦略
- Chrome DevToolsとの統合によるデバッグ体験の向上
これらの機能を適切に活用することで、パフォーマンス、ユーザー体験、開発者体験のすべてを向上させることができます。
ぜひReact 19.2にアップグレードして、新しい可能性を探ってみてください!