useEffectEventで、エフェクトをやり直さずに最新の値を読む方法
useEffectEvent を使うと、エフェクトの中で最新の props や state を読みながら、その値が変わってもエフェクトをやり直さずに済みます。これまでは「依存配列に入れて、余計にやり直す」か「lint を黙らせて外し、古い値を読んでしまう」かのどちらかになりがちでした。
筆者は検証環境で、同じチャット部屋のコンポーネントを TypeScript(.tsx)で3通りに書きました。テーマを切り替えたときに何回つなぎ直すかと、通知にどのテーマが使われるかを調べました。その結果に、公式ドキュメントにある使いどころと制約を合わせてまとめました。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 6.0.3、esbuild 0.28.2、jsdom 29.1.1、eslint 10.13.0、eslint-plugin-react-hooks 7.1.1、typescript-eslint 8.72.0 です。.tsx を tsc で型チェックしてから esbuild で変換し、jsdom の上で描画しました。GIF は、同じコンポーネントを Playwright 1.56.1 の Chromium で動かして撮りました。チャットのサーバーは、検証用に作った偽物です。
困るのは、エフェクトから呼ぶ処理がほかの値も読むとき
useEffectEvent は React 19.2 で入ったフックです(React 19.2 の発表記事)。発表記事は、チャット部屋に接続できたら通知を出す例を使って、問題を説明しています。
通知の見た目にテーマ(theme)を使うと、theme をエフェクトの依存配列に入れることになります。すると、テーマを変えただけでチャットがつなぎ直されます。roomId が変わったときにつなぎ直すのは正しい動きですが、テーマが変わっただけでつなぎ直すのは不自然です。
発表記事によると、よくある対処は、lint ルールを無効にして theme を依存配列から外すことでした。しかし、そうすると、あとでエフェクトを直したときに、lint が依存配列の漏れを教えてくれなくなります。
3つの書き方を同じ手順で動かした
検証では、偽のチャットサーバーを用意しました。このサーバーは、connect() が呼ばれた回数を数えます。サーバーからメッセージを送ると、コンポーネントから showNotification(text, theme) が呼ばれ、通知の文面とテーマが記録されます。
3つの書き方は、あとで見る GIF の画面のラベルに合わせて、①②③と呼びます。
①は、theme を依存配列に入れる書き方です。
export function ChatRoom({ roomId, theme }: Props) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', (text) => {
showNotification(text, theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme が変わるたびに、つなぎ直す
return <h1>{roomId}</h1>;
}
②は、エフェクトの中身を①と同じにしたまま、依存配列を [roomId] にする書き方です。依存配列の直前の行に // eslint-disable-next-line react-hooks/exhaustive-deps を書いて、lint の警告を黙らせます。
③は、useEffectEvent を使う書き方です。通知を出す部分を「エフェクトイベント」として切り出し、エフェクトの中からはそれを呼ぶだけにします。
import { useEffect, useEffectEvent } from 'react';
export function ChatRoom({ roomId, theme }: Props) {
const onMessage = useEffectEvent((text: string) => {
showNotification(text, theme); // いつも最新の theme を読む
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', (text) => {
onMessage(text);
});
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>{roomId}</h1>;
}
動かす手順は、3つとも同じです。まず general の部屋をテーマ light で描画し、メッセージ msg1 を送りました。続いてテーマを dark にして msg2 を、light に戻して msg3 を送りました。最後に、部屋を travel、テーマを dark に変えて msg4 を送りました。出力の ChatWithDeps が①、ChatStale が②、ChatEffectEvent が③です。
$ node run.mjs ./out/ChatWithDeps.mjs
./out/ChatWithDeps.mjs: connects=4 disconnects=3
notes: msg1(light) msg2(dark) msg3(light) msg4(dark)
$ node run.mjs ./out/ChatStale.mjs
./out/ChatStale.mjs: connects=2 disconnects=1
notes: msg1(light) msg2(light) msg3(light) msg4(dark)
$ node run.mjs ./out/ChatEffectEvent.mjs
./out/ChatEffectEvent.mjs: connects=2 disconnects=1
notes: msg1(light) msg2(dark) msg3(light) msg4(dark)
①は、通知のテーマは正しいものの、接続が4回になりました。必要なのは、最初の接続と、部屋を変えたときの2回だけです。残りの2回は、テーマの切り替えによる余計なつなぎ直しです。
②は、接続が2回に減りました。ただし、msg2 の通知が light になっています。テーマはすでに dark なのに、エフェクトを作ったときの古い theme を読み続けたためです。部屋を変えてエフェクトが作り直されると、msg4 では正しいテーマに戻りました。
③は、接続が2回で、通知のテーマもすべて正しいものでした。つなぎ直したのは、部屋を変えたときだけです。通知には、その時点での最新の theme が使われています。
次の GIF は、同じ3つのコンポーネントを Chromium の画面に縦に並べ、いちばん上の「テーマを切り替える」ボタンを3回押したときの様子です。この GIF では部屋は変えず、ボタンを押すたびにメッセージを1つ送っています。
押した直後は、値が変わった欄がピンクの枠と文字になります。①は「接続した回数」が押すたびに増えて4回になり、③は1回のままです。②の「最後の通知」も押すたびにピンクになりますが、theme が dark になっても、通知は (light) のままです。

useEffectEvent を使うと、エフェクトから呼ばれる処理が読む値を、エフェクトをやり直すきっかけから外せます。外した値も、読むときにはいつも最新です。
lint を黙らせた版は警告が消え、useEffectEvent 版は黙らせなくても通る
イベントとエフェクトを切り分ける方法についての公式ページは、依存配列の lint を黙らせないよう勧めています。黙らせると、あとでエフェクトに新しい値を使ったときにも、警告が出なくなるからです。
検証環境の eslint-plugin-react-hooks 7.1.1 でも、②(eslint-disable のコメントつき)には何も出ませんでした。同じコードからコメントだけを外すと、次の警告が出ます。
./src/MissingDep.tsx
15:6 warning React Hook useEffect has a missing dependency: 'theme'. Either include it or remove the dependency array react-hooks/exhaustive-deps
③にも、警告は出ませんでした。useEffectEvent で作った関数は依存配列に入れないものとして扱われ、theme もエフェクトの中で直接は読んでいないためです。
エフェクトイベントの制約と、破ったときの反応
useEffectEvent のリファレンスの注意事項には、使い方の制約が書かれています。エフェクトイベントに特有の要点は、次の3つです。
- エフェクト(
useEffect・useLayoutEffect・useInsertionEffect)の中か、同じコンポーネントの別のエフェクトイベントの中からだけ呼ぶ。描画中に呼んだり、ほかのコンポーネントやフックに渡したりしない。 - 依存配列に書くべき値を省くための道具として使わない。値が変わったらエフェクトをやり直すべきなら、依存配列に残す。
- 返ってくる関数は、描画のたびに意図的に別の関数になる。
制約をわざと破るコードを用意し、lint の結果と、実行したときの動きを確かめました。破り方は、依存配列に入れる、イベントハンドラーから呼ぶ、子コンポーネントに渡す、描画中に呼ぶ、の4つです。
./src/Misuse.tsx
20:15 warning Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `onMessage` from the list react-hooks/exhaustive-deps
23:5 error `onMessage` is a function created with React Hook "useEffectEvent", and can only be called from Effects and Effect Events in the same component react-hooks/rules-of-hooks
29:25 error `onMessage` is a function created with React Hook "useEffectEvent", and can only be called from Effects and Effect Events in the same component. It cannot be assigned to a variable or passed down react-hooks/rules-of-hooks
./src/RenderCall.tsx
5:13 error `onLog` is a function created with React Hook "useEffectEvent", and can only be called from Effects and Effect Events in the same component react-hooks/rules-of-hooks
依存配列に入れた行は警告になり、残りの3つ(イベントハンドラーから呼ぶ、子に渡す、描画中に呼ぶ)はエラーになりました。描画中に呼ぶコードは、lint を無視して実行すると、React 自身が次のエラーを投げました。
RenderCall error: A function wrapped in useEffectEvent can't be called during rendering.
useEffectEvent が返す関数が、描画のたびに変わることも確かめました。ボタンを2回押して計3回描画させ、それぞれ前回の描画の関数と同じかどうかを比べたところ、2回とも別の関数でした。
Identity: renders=3 sameAsPrevious=false,false
そのため、エフェクトイベントを依存配列に入れると、描画のたびにエフェクトがやり直されます。依存配列に onMessage を入れた版を前と同じ手順で動かすと、接続は①と同じ4回でした。useEffectEvent が返す関数が毎回変わるのは、意図的な作りです。リファレンスにある、その理由の説明では、依存配列に入れる誤りを目に見える形にするためだとしています。
lint を黙らせて依存配列から値を外すと、エラーは出ないまま古い値を読みます。検証の msg2(light) のように、見た目では気づきにくい食い違いになります。
どんなときに使うか
React 19.2 の発表記事の、useEffectEvent を使う場面の説明によると、useEffectEvent を使うのは、ユーザーの操作ではなく、エフェクトから発生する「イベント」にあたる処理です。リファレンスの注意事項も、本当にエフェクトから発生するイベントと言える処理にだけ使うよう求めています。発表記事は、すべてを useEffectEvent で包んだり、lint のエラーを消すためだけに使ったりすると、不具合のもとになるとも書いています。
検証の例に当てはめると、つなぎ直すきっかけにすべき roomId は依存配列に残し、通知の見た目にしか関わらない theme はエフェクトイベントの中で読む、という分け方になります。「この値が変わったら、エフェクトをやり直すべきか」と考えると、どちらに置けばよいかが決まります。
エフェクトイベントは、自作のフックの中でも使えます。リファレンスにある、カスタムフックで使う例では、useInterval(callback, delay) の callback をエフェクトイベントで包んでいます。こうすると、毎回新しい callback が渡されても、タイマーを作り直さずに済みます。この例は、検証環境では動かしていません。





