1101文字
6分
編集

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 パターン。

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

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

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

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

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

tsx
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 を使う

tsx
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 を入れないようにするのと何が違うの?

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

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

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

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

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

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

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

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

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

tsx
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

tsx
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 でどう置き換えられるの?

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

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

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

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

  • useEffectOnce ではリアクティブな部分を追加する方法が明らかではない。
    • コードの変更によって何かの値が後から動的になったとしても、そのことに気づくのは難しい。
  • 一度か何回も呼ぶのかを考えるのは React のメンタルモデルにあっていない。
    • 重要なのはそれが reactive なのか non-reactive なのか。
編集