第5回
React 19.2の<Activity>とuseEffectEventを試す
React 19.2で安定版になったActivityとuseEffectEventを実際に試す。タブを切り替えても状態が消えないUIと、useEffectの依存配列の悩み解消を動かしながら解説する。
「React最新ラボ」第5回。2025年10月リリースの React 19.2 から、UIの作り方が変わる2つの新API <Activity> と useEffectEvent を実際に動かして確かめる。実験版ではなく安定版なので、今日から普通に使える。
これまでの「隠す」方法は、どちらも一長一短だった
タブやアコーディオンで「今見えていない部分」をどう扱うか。選択肢は実質2つしかなかった。
条件レンダリング {show && <Tab/>} |
display: none で隠す |
|
|---|---|---|
| state / DOM | 消える | 残る |
| エフェクト(購読・タイマー) | 止まる | 動き続ける |
| 隠れている間の更新 | ─ | 通常の優先度で走り、画面の邪魔をする |
| 復帰の速さ | 一からマウントし直し | 速い |
「stateは残したい。でも裏で動き続けてほしくはない」──その中間がなかった。
<Activity>:隠すけど、捨てない
<Activity> は子要素を mode プロパティで2つの状態に切り替える。
import { Activity } from 'react'
<Activity mode={isActive ? 'visible' : 'hidden'}>
<TabContent />
</Activity>visible:普通に表示。エフェクトもマウントされるhidden:非表示になり、エフェクトはアンマウント。ただしstateとDOMは保持。さらに隠れている間の更新は優先度が下げられ、見えている画面の邪魔をしない
表示+エフェクト稼働
state/DOM保持・エフェクト停止
タブUIに当てはめると、こうなる。
// ./src/App.tsx
import { Activity } from 'react'
import { useState } from 'react'
import { ProfileForm } from './ProfileForm'
import { Settings } from './Settings'
const tabs = ['プロフィール', '設定'] as const
type Tab = (typeof tabs)[number]
export function App() {
const [active, setActive] = useState<Tab>('プロフィール')
return (
<main>
{tabs.map((tab) => (
<button key={tab} onClick={() => setActive(tab)}>
{tab}
</button>
))}
<Activity mode={active === 'プロフィール' ? 'visible' : 'hidden'}>
<ProfileForm />
</Activity>
<Activity mode={active === '設定' ? 'visible' : 'hidden'}>
<Settings />
</Activity>
</main>
)
}ProfileForm の中は普通の useState でいい。何も特別なことをしなくても、タブを離れて戻ってきたときに入力が残っている。
プロフィール 設定
名前:山田 たける
自己紹介:Reactを勉強中です。最近は19.2を追いか
✓ 入力途中の内容がそのまま残っている(アンマウントされていない)
useEffectEvent:エフェクトの「依存配列に入れたくない値」問題
もう一つの新API useEffectEvent は、useEffect を書いたことがある人なら一度はハマる問題を解決する。チャットルームの例で見てみよう。
// ❌ これまで:theme を deps に入れざるを得ない
useEffect(() => {
const conn = createConnection(roomId)
conn.on('connected', () => {
showNotification('接続しました', theme) // theme を使っている
})
conn.connect()
return () => conn.disconnect()
}, [roomId, theme]) // ← theme が変わるだけで再接続されてしまう!やりたいのは「roomId が変わったら接続し直す」だけ。でも通知の見た目に theme を使っているせいでリンタは theme を依存配列に要求し、結果テーマを切り替えるだけでチャットが切断・再接続される。
useEffectEvent は、この「エフェクトの中のイベント的な処理」を切り出すためのフックだ。
import { useEffect, useEffectEvent } from 'react'
// ✅ React 19.2から
const onConnected = useEffectEvent(() => {
showNotification('接続しました', theme) // 常に最新の theme が見える
})
useEffect(() => {
const conn = createConnection(roomId)
conn.on('connected', () => onConnected())
conn.connect()
return () => conn.disconnect()
}, [roomId]) // ← roomId だけ。theme の変更では再接続されないuseEffectEvent で作った関数は:
- 常に最新のprops / stateを読める(古い値を掴む stale closure が起きない)
- 依存配列に入れない。最新の
eslint-plugin-react-hooksはこれを正しく理解して、警告を出さない
使うときの注意
useEffectEventはエフェクトの中から呼ぶためのもの。propsとして子に渡したり、イベントハンドラの代わりに使うのは想定外の使い方- 依存配列には入れない。入れると意味がなくなる。リンタが古いと逆に「入れろ」と誤警告するので、
eslint-plugin-react-hooksを最新版にしてから使う <Activity>のhiddenはアンマウントではない。stateが保持され続けるぶんメモリは使う。無限に積むリスト等では使いどころを考える- 対応バージョンは React 19.2以降。
package.jsonのreact/react-domを確認してから試す
まとめ
- React 19.2の
<Activity>は「隠すけど捨てない」。hiddenでstateとDOMを保持したままエフェクトを止め、更新を低優先度にする - タブ・ウィザード・プリレンダリングなど「戻ってきたら続きから」のUIがコンポーネント1つで書ける
useEffectEventはエフェクト内のイベント的処理を切り出し、依存配列から解放する。stale closureも起きない- どちらも安定版API。
eslint-plugin-react-hooksを最新にしてから導入する
次回は視点を変えて、いま一番勢いのあるフルスタックReactフレームワーク TanStack Start(v1リリース候補)を試す。第2回のWakuとの違いにも注目してほしい。