[React] カスタムフック useToggle 内で useState を使うときに気をつけなければいけないこと

カスタムフック useToggle を例に useState で値を更新する関数を使うときに気をつけなければいけないことを考えてみます。
2021.04.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ON と OFF を切り替える useToggle というカスタムフックを例を交えながら作ってみます。

よくない例 1

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);

  const toggle = () => setState(!state);

  return [state, toggle] as const;
};

トグルなので実際にはあまり問題が起こりそうではないのですが、何度も急激に実行された場合に古い state の値を参照してしまうことが考えられます。

よくない例 2

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);

  const toggle = () => setState((prevState) => !prevState);

  return [state, toggle] as const;
};

よくない例 1 も同様なのですが、toggle を useCallback で包んでいないので問題がありそうです。toggle が子のコンポーネントに渡された場合、再レンダリングされてしまいます。

こちらについてはカスタムフックを作るときにこころがけていること という記事でも以前に書いておりますのでご参照ください。

よくない例 3

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);

  const toggle = useCallback(() => {
    setState(!state);
  }, [state]);

  return [state, toggle] as const;
};

こちらは useCallback をしているけれども、prev state を参照していない例になります。

state の変化によって再定義されてしまっているため、以下のような使い方をしてみると……。

const [checked, toggle] = useToggle();

useEffect(() => {
  toggle();
}, [toggle]);

無限ループしてしまいました。

よい例 1

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);

  const toggle = useCallback(() => {
    setState((prevState) => !prevState);
  }, []);

  return [state, toggle] as const;
};

useCallback と prev state を使用していて問題なさそうです。

よい例 2

useState を使わず、useReducer を使ったトグルの実装もできます。タイトル詐欺になってしまいますが……。

const useToggle = (initialValue = false) => {
  return useReducer((state) => !state, initialValue);
};

react-use の実装もこちらとほぼ同等になっています。

まとめ

非常にシンプルな機能をもたせたカスタムフックも、安直に実装してしまうと場合によってはよくない実装になってしまうことがあるので気をつけたいです。