Fire-and-forgetパターンと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 なのか。