useEffectEvent フックを使って useEffect ともっと上手く付き合おう
useEffectEvent という react フックをご存知ですか? まだ experimental なので、知らない方も多いと思います。しかし、このフックは 「なんで今までなかったんだろう?」と思ってしまうほど革新的 です。今回はその使い方の紹介などをします。
概要: useEffectEvent は useEffect とともに使うフック
まず概要ですが、useEffectEvent は イベントリスナーを設定する useEffect とセットで使うフック です。
useEffectEvent を使うと、エフェクトとイベントリスナーを分離できます。そして、イベントリスナーの deps の変化時にエフェクトを再実行せずに済みます。
…とまあ、抽象的な説明だけでは分かりづらい と思うので、以降では useEffectEvent がどういう課題を解決するのか、また具体的にどういうケースで使われるのかなどを見ていきます。
解決したい課題: useEffect の不要な再実行を減らしたい
「特定のキーが押された時に処理を行うカスタムフック」を定義してみます。
const useKeydown = (targetKey: string, handler: () => void, isEnabled = true) => {
useEffect(() => {
if (!isEnabled) {
return;
}
const keydownListener = (e: KeyboardEvent) => {
if (e.key === targetKey) {
handler();
}
};
document.addEventListener('keydown', keydownListener);
return () => {
document.removeEventListener('keydown', keydownListener);
};
}, [isEnabled, targetKey, handler]);
};
一見、実装できてそうです。しかし、この実装には イベントの deps とイベントリスナーの deps が混ざってしまっている という問題があります。
useEffect(() => {
// ...
}, [isEnabled, targetKey, handler]); // リスナーの deps まで入っている (targetKey と handler)
つまり、isEnabled は変わらずとも targetKey が変わればエフェクトが再実行されます。これは 実装都合で必要なだけで、本来要らないエフェクトの再実行です。
この問題を解消するのが useEffectEvent フックです。
具体的なユースケース: useEffect でイベントリスナーを設定する
useEffectEvent を使って先ほどの「特定のキーが押された時に処理を行うカスタムフック」を定義してみます。
// targetKey が押されたら handler を実行するフック
const useKeydown = (targetKey: string, handler: () => void, isEnabled = true) => {
// イベントリスナーを定義
const keydownListener = useEffectEvent((e: KeyboardEvent) => {
if (e.key === targetKey) {
handler();
}
});
// keydown イベントを監視
useEffect(() => {
if (!isEnabled) {
return;
}
document.addEventListener('keydown', keydownListener);
return () => {
document.removeEventListener('keydown', keydownListener);
};
}, [isEnabled]);
};
こちらのコードでは、handler が変化してもエフェクトは再実行されず、さらに常に最新の handler が参照されています。
このように、useEffectEvent を使うと イベントリスナーの定義とリスナーを貼るエフェクトが分離され、エフェクトの不要な再実行がなくなります。 これが useEffectEvent の役割です。
補足: useCallback ではダメなのか?
keydownListener は useCallback でも実装できそうな雰囲気があります。
const keydownListener = useCallback((e: KeyboardEvent) => {
if (e.key === targetKey) {
handler();
}
}, [targetKey, handler]);
しかし、これでは結局 当初の問題が解決できていません。 というのも、targetKey が変わると keydownListener が変化し、結果としてエフェクトが再実行されます。そのため、useCallback ではなく useEffectEvent でなければいけません。
(useEffectEvent の polyfill 実装には useCallback も使われています)
嬉しい点: 不要なエフェクトの再実行を減らせる
上のコード内の useEffect は isEnabled のみに依存しています。もし useEffectEvent を使わず実装していたら、targetKey など余計な依存が deps に入っていました。
useEffect(() => {
// ...
}, [isEnabled]); // 余計な依存が一切ない
useEffect に余計な依存が入ってないので、このコードは「イベントリスナーは isEnabled が変化した時にだけ貼られる」というシンプルなコードになっています。余計な心配(「targetKey の変化時にもエフェクトが走るけど、実行タイミング次第でバグらないかな…」とか)が発生しないので、心理的にもとても楽です。
余計な依存がない、つまり エフェクトの不要な実行を減らせる点が useEffectEvent の最も嬉しいポイント です。
まとめ
useEffectEvent は異なる二つの関心ごと(「エフェクトをいつ実行するか」「イベントリスナーが何に依存するか」)をうまく分離してくれます。
それにより、以下のような恩恵があります
- イベントリスナーが変化しても deps の更新が不要になる
- 意図したタイミングでのみエフェクトが実行され、コードがシンプルになる
useEffectEvent は今後標準となるはずです。今のうちにその考え方を履修しておきましょう。
余談
useEffectEvent は元々 useEvent という名前でした。用途も微妙に異なっていましたが、そこから変更されたようです。
すでに polyfill が存在していて、概ね問題なく使えます(細かい挙動は異なります)。
Discussion