エフェクトイベントとは何か?useEffectEventを理解する
React 19.2からuseEffectEventというフックが追加されました。
useEffectEventについて、公式ドキュメントでは次のように説明されています。
useEffectEvent は、エフェクトから非リアクティブなロジックを、エフェクトイベント (Effect Event) と呼ばれる再利用可能な関数へと抽出できるようにする React フックです。
参照:https://ja.react.dev/reference/react/useEffectEvent
本記事では、「エフェクトイベント」とは何かを確認しながら、useEffectEventについて紹介したいと思います。
エフェクトとイベントハンドラ
エフェクトイベントを理解するには、まずエフェクトとイベントハンドラの違いを整理する必要があります。その鍵になるのが、リアクティブな値という概念です。
Reactでは、リアクティブな値を次のように定義しています。
コンポーネントの本体部分で宣言された props、state および変数のことをリアクティブな値 (reactive value) と呼びます。
参照:https://ja.react.dev/learn/separating-events-from-effects#reactive-values-and-reactive-logic
コンポーネント内で宣言されたこれらの値は、レンダー時に計算される値です。そのため、再レンダーに応じて変化する可能性があります。
たとえば次のコードを見てみましょう。このコードは、チャットルームへの接続処理とメッセージ送信を行うコンポーネントです。
const SERVER_URL = 'https://localhost:1234';
const ChatRoom = ({ roomId }) => {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(SERVER_URL, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
const handleSendClick = () => {
sendMessage(message);
};
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
};
上記のコードでは、messageやroomIdはリアクティブな値です。一方で、コンポーネントの外で定義されたSERVER_URLはリアクティブな値ではありません。
ここで重要なのは、エフェクトとイベントハンドラでは、リアクティブな値への対応が異なるということです。この違いを把握することが、エフェクトイベントを理解するための出発点となります。
もう少し詳しく見ていきましょう。
リアクティブなロジックであるエフェクト
エフェクトは、レンダーによって引き起こされる副作用です。Reactでは、コンポーネントを外部システムと同期させるためにuseEffectを使います。公式ドキュメントでも、次のように説明されています。
useEffect は、コンポーネントを外部システムと同期させるための React フックです。
参照:https://ja.react.dev/reference/react/useEffect
ここで、チャットルームへの接続処理に着目してみましょう。
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
このコードでは、roomIdが変更されるたびに接続処理が再実行されます。接続先のチャットルームはroomIdによって決まるため、roomIdが変わったときには、Reactは古い接続を切断し、新しい roomIdで再同期する必要があります。
ここでポイントになるのが、roomIdがリアクティブな値だということです。
エフェクト内でリアクティブな値を読み取る場合、その値を依存配列に含める必要があります。そうすることで、Reactはその値の変化を検知し、必要に応じてエフェクトを再実行できます。
このように、エフェクトはリアクティブな値の変化に応じて自動的に再実行されるため、リアクティブなロジックだといえます。
エフェクト内のロジックはリアクティブである。エフェクトがリアクティブな値を読み取る場合、依存配列としてそれを指定する必要がある。その後再レンダーによって値が変化した場合、React は新しい値でエフェクトのロジックを再実行する。
参照:https://ja.react.dev/learn/separating-events-from-effects#logic-inside-effects-is-reactive
非リアクティブなロジックであるイベントハンドラ
イベントハンドラとは、クリックやホバーなどのユーザーのインタラクションに応じて実行される関数のことです。たとえば送信ボタンのクリックのように、特定の操作をきっかけに実行されます。
以下はボタンをクリックした際に、messageを送信するコードの例です。
const ChatRoom = () => {
const [message, setMessage] = useState('');
const handleSendClick = () => {
sendMessage(message);
};
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
};
このコードでは、messageはリアクティブな値ですが、その値が変わっただけではhandleSendClick()は実行されません。handleSendClick()呼ばれるのは、あくまでユーザーが送信ボタンをクリックしたときです。つまり、実行のきっかけはレンダーではなく、ユーザー操作にあります。
このように、イベントハンドラは実行時にリアクティブな値を読み取れますが、その値の変化に応じて自動的に再実行されるわけではありません。したがって、イベントハンドラは非リアクティブなロジックだといえます。
イベントハンドラ内のロジックはリアクティブではない。ユーザが同じ操作(クリックなど)を再度行わない限り、再度実行されることはない。イベントハンドラは値の変化に「反応」することなく、リアクティブな値を読み取ることができる。
参照:https://ja.react.dev/learn/separating-events-from-effects#logic-inside-event-handlers-is-not-reactive
エフェクトイベントとは
ここまで、エフェクトはリアクティブなロジック、イベントハンドラは非リアクティブなロジックという異なる性質を持つことを確認しました。しかし実際のコードでは、エフェクトの中に非リアクティブな処理を含めたくなることがあります。
エフェクトの中に混ざる非リアクティブなロジック
チャットルームに接続した際に、現在のテーマに合わせた見た目で通知を表示したいとしましょう。接続処理のエフェクト内でthemeも参照すると、次のようなコードを書くと思います。
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('message', () => {
showNotification('Connected!', theme);
});
return () => connection.disconnect();
}, [roomId, theme]);
themeはpropsでありリアクティブな値なので、エフェクトの依存配列に含める必要があります。
themeが依存配列に追加されると、themeが変わるたびにエフェクト全体が再実行されます。結果として、通知の見た目を変えたいだけなのにもかかわらず、不要なチャットの再接続が生じてしまいます。
問題は、roomIdに応じて再同期すべき接続処理と、通知を表示するだけの非リアクティブな処理が、同じエフェクトの中に混在していることです。
通知処理は最新のthemeを参照できれば十分であり、themeの変更に合わせて接続処理まで再同期する必要はありません。
useEffectEventによるエフェクトとイベントの分離
ここでuseEffectEventを使うと、エフェクトの中に含まれていた非リアクティブな処理を分離することができます。先ほどのコードを書き直してみましょう。
const onMessage = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('message', onMessage);
return () => connection.disconnect();
}, [roomId]);
useEffectEventを使うことで、次のように整理できます。
- 接続処理は
roomIdにのみ依存するようになり、themeが変わっても再実行されない -
themeは依存配列から外れるが、onMessageは呼び出された時点の最新のthemeを参照できる - 通知処理を、非リアクティブなロジックとして明確に分離できる
ここでuseEffectEventによって分離されたonMessageのことを、エフェクトイベントと呼びます。エフェクトイベントについて、公式ドキュメントでは次のように説明しています。
これはエフェクトロジックの一部でありながら、むしろイベントハンドラに近い動作をします。この中のロジックはリアクティブではなく、常に props と state の最新の値を「見る」ことができます。
参照:https://ja.react.dev/learn/separating-events-from-effects#declaring-an-effect-event
エフェクトイベントは、エフェクトの一部でありながら、イベントハンドラのように非リアクティブに振る舞うロジックだと捉えられます。
イベントハンドラとの違いは、その実行のきっかけにあります。イベントハンドラはユーザー操作によって実行されますが、エフェクトイベントはエフェクトの中から呼び出されます。
useEffectEventを使うことで、リアクティブであるべき接続処理はエフェクトに残したまま、リアクティブである必要のない通知処理だけを切り離せます。つまり useEffectEventは、エフェクトの中に混在したリアクティブな処理と非リアクティブな処理を切り離すためのフックです。
気をつけるべきこと
最後にuseEffectEventを使用する際の注意点について簡単に触れておきます。
公式ドキュメントでは、主に以下の3点が挙げられています。
- エフェクト内でのみ呼び出す
- 依存配列を避けるためのものではない
- 非リアクティブなロジックだけに使う
参照: https://ja.react.dev/reference/react/useEffectEvent#caveats
これらは、本記事で紹介したようにuseEffectEventがエフェクト内の非リアクティブな処理を切り離すためのフックだと捉えると理解しやすいと思います。
また、eslint-plugin-react-hooks のようなリントルールによって、機械的に検知できるケースもあります。リントでエラーが出た場合は、これらの注意点に反していないかを確認すると良いでしょう。
おわりに
本記事では、エフェクトイベントという概念をもとにuseEffectEventについて紹介しました。エフェクトはリアクティブ、イベントハンドラは非リアクティブという性質の違いを整理したうえで、エフェクトイベントが何であるかを把握できるとuseEffectEventの使い方について、理解が深まるのではないかと思います。
本記事の内容が、参考になれば幸いです。
参考
Discussion