useEffectEvent

useEffectEvent で Effect の再同期と最新値を読む処理を分離する方法を学ぶ。

useEffectEvent は、Effect から呼び出す「非リアクティブなイベント」を作る Hook です。Effect を再接続せずに、イベント発火時点の最新の props や state を読みたい場合に使います。

接続条件と通知を分ける

チャットの部屋だけが変わったとき接続し直し、テーマの変更では通知の見た目だけ最新にしたい例です。

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
  const onConnected = useEffectEvent(() => showNotification('接続しました', theme));

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <p>{roomId} に接続中</p>;
}

onConnected を通常の関数としてコンポーネント内に置くと、ESLint の Hooks ルールは theme と関数自体を Effect の依存へ求めます。するとテーマを変えるだけで接続が切断・再接続されます。Effect Event は「接続時の通知は最新テーマを読むが、テーマは接続条件ではない」という意図を API で表します。

onConnected は最新の theme を読みますが、theme の変更で接続を作り直しません。再同期の条件である roomId は依存配列に残します。

ここで重要なのは「接続をいつ作り直すか」と「接続した瞬間に何をするか」が別の関心である点です。roomId は接続そのものを決める reactive な値です。一方 theme は通知の見た目だけを決めるため、接続イベントが発火した時点で最新なら十分です。

依存配列を省くためには使わない

Effect を再実行すべき値を useEffectEvent に隠すと、同期漏れのバグになります。Effect Event は Effect 内または別の Effect Event からだけ呼び、props として子へ渡したり、クリックハンドラとして使ったりしません。

返された関数は useCallback のような安定した関数ではありません。依存配列へ入れることも、汎用的なイベントハンドラとして使い回すこともできません。まず Effect を「外部システムとの同期」として小さく設計し、その中にだけ残る非リアクティブなイベントを切り出します。

stale closure を万能に解決するものではない

古い state を読む問題を見つけても、すぐ useEffectEvent へ置き換えないでください。クリックに応じた更新ならイベントハンドラで updater 関数を使う、外部購読なら依存配列を正しく書く、という基本の方が適切なことが大半です。Effect Event は「Effect が購読し続ける外部イベント」でだけ検討します。

まとめ

useEffectEvent は最新値を読むことと外部システムを再同期することを分離します。まず useEffect の依存関係を正しく書き、発火時だけ必要な処理に限定して使いましょう。

Sources