本文へスキップ
Dev Dailyエンジニア デイリーニュース

RELEASE · React

React 19.2 解説 — Activity・useEffectEvent・cacheSignal で UI 保持と副作用を整理する

React 19.2 は <Activity> による UI 状態の保持、安定化した useEffectEvent、Server Components 向けの cacheSignal、ストリーミング SSR の Suspense バッチングなどを追加したマイナーリリースです。新 API の使いどころと移行時の注意点をまとめます。

執筆: kinjo8 分で読了計画的に更新マイナーJavaScript

新 API は破壊的変更ではなく追加が中心。useEffectEvent の手書き実装がある場合のみ置き換えを検討すれば移行は穏当です。

目玉機能

このリリースで追加・改善された主な機能。

<Activity> でUIをマウントしたまま隠せる

visible / hidden の2モードを持ち、hidden では effect をアンマウントしつつ DOM と state を保持します。タブ切替やモーダルで state を捨てずに済みます。

useEffectEvent が安定版に

最新の props/state を読みつつ useEffect の依存配列に含めなくてよい関数を定義できます。リスナの再登録が減り、依存配列の肥大化を抑えられます。

cacheSignal で Server Components のリソース後始末

cache() で囲んだリソースの寿命が尽きたときに発火する AbortSignal を取得でき、バックグラウンド処理を確実に中断できます。

ストリーミング SSR で Suspense をバッチ表示

境界を1つずつ出すのではなくグループ単位でまとめて表示し、クライアント側に近い見え方になります。レイアウトのちらつきが減ります。

破壊的変更

アップグレード時に確認すべき非互換の変更点。移行方法も併記します。

eslint-plugin-react-hooks の useEffectEvent ルール対応

useEffectEvent で定義した関数を effect やコールバック以外へ渡すとlintで警告されます。実験的フックを独自実装していた場合は公式版へ寄せます。

移行 package.json の eslint-plugin-react-hooks を更新し、自前の useEffectEvent 相当を import 置換します。

スコアカード

DL 数 / スター / バンドルサイズ変化など、このリリースの定量データ。

min+gzip (react-dom)

約 +0.4KB

Activity 追加に伴う微増。体感差はほぼなし

週間 npm DL (react)

約 3,500 万

メジャー級の普及。19系へ追従が進む

新規 public API

3 件

Activity / useEffectEvent / cacheSignal

React 19.2 は、19 系の路線を踏襲しつつ実用機能を積み増したマイナーリリースです。目玉は UI の状態を保持したまま隠せる <Activity>、安定版になった useEffectEventServer Components 向けの cacheSignal、そしてストリーミング SSR の Suspense バッチングです。いずれも破壊的変更ではなく追加が中心で、既存コードを壊しにくい構成になっています。

このリリースの位置づけと、各 API の使いどころを順に見ていきます。

このリリースの全体像

19.2 は「19.0 で入った React Server Components と Actions の路線を、開発者体験 (DX) と細かな描画制御の面で補強する」回です。新しいパラダイムを導入するのではなく、現場でよく踏む摩擦をいくつか減らしにきています。

facebook/react のリリースノートを見ると、追加された public API は 3 件 (Activity / useEffectEvent / cacheSignal) に絞られており、残りは SSR の挙動改善やデバッグ支援です。アップグレード自体は穏当ですが、useEffectEvent を実験的フックとして自前で使っていたチームだけは置き換え作業が発生します。

Activity — 隠しても state を捨てない

<Activity> は、子ツリーを「見える (visible)」か「隠す (hidden)」かで制御するコンポーネントです。従来の条件付きレンダリング ({show && <Panel />}) との違いは、hidden にしても DOM と state が保持される点です。

import { Activity } from 'react'

function Tabs({ active }) {
  return (
    <>
      <Activity mode={active === 'editor' ? 'visible' : 'hidden'}>
        <Editor />
      </Activity>
      <Activity mode={active === 'preview' ? 'visible' : 'hidden'}>
        <Preview />
      </Activity>
    </>
  )
}

hidden のときは effect がアンマウントされ、更新は React が他の作業を終えるまで遅延されます。visible に戻すと effect が再マウントされ、入力中のフォーム値やスクロール位置はそのまま残ります。タブ UI・ウィザード・モーダルなど「一度開いたら state を捨てたくない」場面で効きます。

注意点として、hidden の間も DOM はツリーに残るため、巨大なリストを大量に隠し持つとメモリは消費します。常時マウントが妥当なケースか、毎回作り直すべきかは中身の重さで判断します。

useEffectEvent — 依存配列から「最新値を読む処理」を切り離す

useEffectEvent は、effect の中から「常に最新の props/state を参照したいが、その値が変わっても effect を再実行させたくない」関数を定義するためのフックです。

import { useEffect, useEffectEvent } from 'react'

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showToast(`接続しました (${theme})`)
  })

  useEffect(() => {
    const conn = connect(roomId)
    conn.on('connected', () => onConnected())
    return () => conn.disconnect()
  }, [roomId]) // theme は依存に入れなくてよい
}

ここで themeonConnected の中で最新値が読まれますが、useEffect の依存配列には入れません。そのため theme を切り替えても接続が張り直されることはなく、roomId が変わったときだけ再接続されます。依存配列の肥大化と、それに伴う不要な再購読・再登録を減らせるのが利点です。

「effect の依存に入れたくないが最新を読みたい」という頻出パターンに公式の答えが用意された形です。eslint-plugin-react-hooks も対応し、useEffectEvent で作った関数を effect やコールバック以外へ渡すと警告されます。

cacheSignal — Server Components のリソース後始末

cacheSignal は React Server Components 専用で、cache() で囲ったリソースの寿命が尽きたタイミングで発火する AbortSignal を返します。キャッシュに紐づくバックグラウンド処理 (フェッチや購読) を、キャッシュ破棄に合わせて確実に中断できます。

import { cache, cacheSignal } from 'react'

const getData = cache(async (id) => {
  const signal = cacheSignal()
  const res = await fetch(`/api/items/${id}`, { signal })
  return res.json()
})

レンダリングがアンマウントされてキャッシュエントリが不要になると signal が abort され、進行中の fetch が中断されます。これにより、もう誰も待っていないリクエストが裏で走り続ける状況を避けられます。クライアント側 (ブラウザのコンポーネント) では使えない点に注意してください。

ストリーミング SSR で Suspense をバッチ表示

サーバーストリーミング時の Suspense の見え方も改善されました。これまでは解決した境界が 1 つずつ順に差し込まれていましたが、19.2 では準備できた境界をグループ単位でまとめて表示するようになりました。クライアントサイドレンダリングに近い挙動になり、コンテンツが小刻みにパッパッと現れることによるレイアウトのちらつきが抑えられます。

この変更はアプリ側のコード修正を必要としません。renderToPipeableStream などでストリーミング SSR を使っていれば、アップグレードするだけで見え方が滑らかになります。

デバッグ: Performance Tracks

開発体験面では、Chrome DevTools の Performance パネルに React 専用のトラックを表示する Performance Tracks が加わりました。コミットやレンダリングのフェーズがタイムライン上で可視化され、どこで時間を使っているかを既存のブラウザツールの中で追えます。React DevTools の Profiler と併用すると、描画コストの当たりをつけやすくなります。

アップグレード方針

19.0 / 19.1 からの移行であれば、reactreact-dom を 19.2 へ上げるだけで大きな破壊はありません。確認しておきたいのは次の点です。

  • 自前で useEffectEvent 相当を実装していた場合は、公式版へ置き換える。eslint-plugin-react-hooks も更新する。
  • ストリーミング SSR を使っている場合は、Suspense の表示タイミングが変わるため、目視で表示順を一度確認する。
  • Activity は新規 API なので導入は任意。既存の条件付きレンダリングを急いで置き換える必要はありません。

総じて、すぐ全面採用すべき機能というより、計画的に取り込めば確実に効いてくる追加群です。まずは検証環境で 19.2 に上げ、useEffectEvent のlint対応を済ませてから本番へ進めるのが無理のない進め方です。

参考リンク

  • 公式リリースノート: facebook/react
  • React 公式ブログ: React 19.2 (react.dev)
  • API リファレンス: react.dev/reference/react

公式リンク

一次情報。最新の仕様はこちらを確認してください。

この記事を共有:でポストはてブ

関連するデイリーニュース

このリリースを取り上げた平日のノート。各日の詳細はこちら。

注記: 本記事は公開情報をもとにした技術情報の提供を目的としています。 最新の仕様や挙動は必ず一次情報 (公式ドキュメント・リリースノート) をご確認ください。