1101 文字
6 分

Fire-and-forgetパターンとuseEvent

2022-09-29

useEvent の RFC はなくなりました。よりスコープを絞ったものを追加するか、ビルド時の最適化で行うことを検討しているようです。- https://github.com/reactjs/rfcs/pull/220#issuecomment-1259938816

お気持ち#

useEffectOnce みたいなコード書きたくなることあると思うけど、一回実行するかどうかという考えは React のメンタルモデルに即していないと思うし、何に反応するかに置き換えられるはずなので、はやく useEvent が入るといいねという気持ち- https://x.com/ohirunewani/status/1523208462397751296

useEffectOnce ってなに?#

このようなカスタムフックス、そしてこのパターンが fire-and-forget パターン。

const executedRef = useRef(false);
useEffect(() => {
if (executedRef.current === false) {
doSomething();
executedRef.current = true;
}
}, []);

なぜ fire-and-forget パターンが必要になるの?#

  • useEffect 内のすべての値は依存関係とみなされ、値が変更されるたびに effect を再起動する。
    • なぜ?effect の結果が常に最新の props や state と一致するようにするため
  • しかし常に再起動してほしいわけではないから

再起動を抑制したいケース#

ページ遷移に反応して特定のメソッドを呼びたい。 具体的には、パフォーマンス計測サービスやアクセス解析サービスなどにデータを送信するメソッド

useEffect(() => {
onVisitPageOnly(
route.url,
[currentUser.name](http://currentuser.name/)
)
},[route.url]) // Missing dependencies: currentUser

Linter に従うとどうなるか#

ページ遷移したときだけでなく currentUser が変更された場合も effect が再起動してしまう。 このような状況で fire-and-forget パターンが有効

  • ただし fire-and-forget パターンは分かりにくい
  • 本当にやりたいことではない
    • 本当にやりたいことは、useEffect 内に route.url のみを含めること

useEvent を使う#

const onVisit = useEvent(url => {
onVisitPageOnly(
route.url,
[currentUser.name](http://currentuser.name/) // 呼び出されたときに最新のユーザー前を取得する
)
})
useEffect(() => {
onVisit(route.url)
},[route.url]) //ここにonVisitを入れる必要はなく、入れたとしても再起動しない

useEvent vs Linter の抑制#

useEvent を使うのと、linter を抑制して currentUser を入れないようにするのと何が違うの?

  • 抑制することはバグの原因になるので避けるべき
useEffect(() => {
onVisitPageOnly(
route.url,
[currentUser.name](http://currentuser.name/)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
},[route.url])

Linter を抑制するとバグるケース#

1 秒毎に下書きを保存したい。

const [text, setText] = useState("");
useEffect(() => {
const id = setInterval(() => {
setText(text);
}, 1000);
return () => clearInterval(id);
}, [text]);

これにハマるのは初学者だけなので、あまり良い例ではない。

キー入力のたびに保存間隔がリセットされてしまう#

const [text, setText] = useState("");
useEffect(() => {
const id = setInterval(() => {
setText(text);
}, 1000);
return () => clearInterval(id);
}, []); // Missing dependencies

依存関係からテキストを取り除いてしまうと saveDraft は常に初期テキストを受け取ってしまう

Linter を抑制する代わりに useEvent を使う#

const [text, setText] = useState("");
const onTick = useEvent(() => {
setText(text);
});
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, []);

useEvent がもたらすもの#

useEvent は React hooks の missing piece

  • Linter の抑制をするなどして無理矢理回避していたパターンを抑制せずに且つ簡単に書けるようになる。
  • 制御の難しい fire-and-forget パターンを扱う必要がほぼなくなる。

Q. useEvent って何をしているの?#

https://github.com/reactjs/rfcs/blob/d85e257502a43c08d17e8ab58efa0880f7f007a5/text/0000-useevent.md#internal-implementation

function useEvent(handler) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}

似たカスタムフックの例として useMethod、こちらはレンダリング中にスローしない。 https://github.com/pie6k/use-method/blob/master/src/index.ts

Q. 結局 useEffectOnce は useEvent でどう置き換えられるの?#

次のように置き換えられる。

const onMount = useEvent(() => {
// whatever
});
useEffect(() => {
onMount();
}, []);

Q. useEffectOnce を使うより useEvent を使った方がいいの?#

useEffectOnce は easy だが React がよく分かっていない人が使うべきものではない。

  • useEffectOnce ではリアクティブな部分を追加する方法が明らかではない。
    • コードの変更によって何かの値が後から動的になったとしても、そのことに気づくのは難しい。
  • 一度か何回も呼ぶのかを考えるのは React のメンタルモデルにあっていない。
    • 重要なのはそれが reactive なのか non-reactive なのか。
Fire-and-forgetパターンとuseEvent
https://blog.ohirunewani.com/posts/fire-and-forget-pattern-and-useevent/
作者
hrdtbs
公開日
2022-09-29
ライセンス
CC BY-NC-SA 4.0