O hirunewani blog

Fire-and-forgetパターンとuseEvent

Created at

Fire-and-forgetパターンつまり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って何をしているの?

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なのか。