Fire-and-forgetパターンとuseEvent
Fire-and-forgetパターンつまりuseEffectOnceのようなコードをuseEventでどのように置き換えられるか
- # お気持ち
- # useEffectOnceってなに?
- # なぜfire-and-forgetパターンが必要になるの?
- # 再起動を抑制したいケース
- # Linterに従うとどうなるか
- # useEventを使う
- # useEvent vs Linterの抑制
- # Linterを抑制するとバグるケース
- # キー入力のたびに保存間隔がリセットされてしまう
- # Linterを抑制する代わりにuseEventを使う
- # useEventがもたらすもの
- # Q. useEventって何をしているの?
- # Q. 結局useEffectOnceはuseEventでどう置き換えられるの?
- # Q. useEffectOnceを使うよりuseEventを使った方がいいの?
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って何をしているの?
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なのか。