第5回

React 19.2の<Activity>とuseEffectEventを試す

React 19.2で安定版になったActivityとuseEffectEventを実際に試す。タブを切り替えても状態が消えないUIと、useEffectの依存配列の悩み解消を動かしながら解説する。

·15分で読める
たける
たける タブUIを作ったんですけど、タブを切り替えると入力フォームの内容が消えるってクレームが来ました。条件レンダリングで出し分けてるので、切り替えた瞬間にアンマウントされて state ごと消えるんですよね……。
りこ
りこ まさにそのための機能が **React 19.2** で安定版に入った。``──画面から「隠す」けど、stateとDOMは保持しておけるコンポーネント。もう一つの新顔 `useEffectEvent` と合わせて、今回は19.2の目玉を2つ試す。

「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は保持。さらに隠れている間の更新は優先度が下げられ、見えている画面の邪魔をしない
visible
表示+エフェクト稼働
hidden
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を追いか

✓ 入力途中の内容がそのまま残っている(アンマウントされていない)

なつみ
なつみ UX的にはもう一つ嬉しい使い方があって、**次に見そうな画面を `hidden` で先にレンダリングしておく**プリレンダリングね。隠れている間の描画は低優先度だから、今の画面を邪魔せずに裏で準備しておいて、タブを開いた瞬間パッと出せる。
たける
たける エフェクトがアンマウントされるってことは、hidden中はタイマーやWebSocket購読は止まるんですね。`display: none` の「裏で動き続ける」問題も同時に解決してるのか。

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 はこれを正しく理解して、警告を出さない
ユナ
ユナ 設計として見ると「エフェクト=リアクティブな同期処理」と「イベント=その瞬間の値で走る処理」を分離する道具ね。依存配列は前者だけを見ればよくなる。これまで `useRef` に関数を入れて誤魔化していたパターンは、全部これで置き換えられる。

使うときの注意

  • useEffectEvent はエフェクトの中から呼ぶためのもの。propsとして子に渡したり、イベントハンドラの代わりに使うのは想定外の使い方
  • 依存配列には入れない。入れると意味がなくなる。リンタが古いと逆に「入れろ」と誤警告するので、eslint-plugin-react-hooks を最新版にしてから使う
  • <Activity> のhiddenはアンマウントではない。stateが保持され続けるぶんメモリは使う。無限に積むリスト等では使いどころを考える
  • 対応バージョンは React 19.2以降package.jsonreact / react-dom を確認してから試す
たける
たける タブ問題は `` で囲むだけ、再接続問題は `useEffectEvent` に切り出すだけ。どっちも「今まで無理やり回避してた問題」に名前と正式な道具が付いた感じですね。
りこ
りこ 回避策を捨てて公式の道具に乗り換える。それがバージョンを追いかける一番の理由。

まとめ

  • React 19.2の <Activity> は「隠すけど捨てない」。hidden でstateとDOMを保持したままエフェクトを止め、更新を低優先度にする
  • タブ・ウィザード・プリレンダリングなど「戻ってきたら続きから」のUIがコンポーネント1つで書ける
  • useEffectEvent はエフェクト内のイベント的処理を切り出し、依存配列から解放する。stale closureも起きない
  • どちらも安定版API。eslint-plugin-react-hooks を最新にしてから導入する

次回は視点を変えて、いま一番勢いのあるフルスタックReactフレームワーク TanStack Start(v1リリース候補)を試す。第2回のWakuとの違いにも注目してほしい。

#react#react19#activity#useeffectevent#intermediate