🧊

「ReactのカスタムHooksをカジュアルに使う」という記事を読んでカジュアルに使ってみた

2023/04/24に公開

元ネタ

https://sbfl.net/blog/2020/08/21/use-react-hooks-easy/

カスタムフックとは

カスタムHookとは何かですが、公式(カスタムフックの抽出)によるとこんな感じの説明がされてます。

  • カスタムフックとは、名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のことです。
  • 自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。

など、具体例とともに色々書いてあるのですが、僕は初見でみたとき何も分からなかったです。

超簡単にいうと、"use"から始まる関数ということだけ理解しました。

まだイメージが涌きづらいかなと思うので、簡単なカウンターの例から一緒にみていきます。

カウンターコンポーネントにカスタムフックを使って作ってみる

普通に書くと以下のようなコンポーネントが出来上がると思います。

普通に書く

const CounterComponent = () => {
  const [count, setCount] = useState(0);

  const addCount = () => {
    //コンポーネント的にはsetCountの処理は別に必要ない
    setCount((prev) => prev + 1);
  };

  return (
    //必要な要素は画面に表示する{count}とonClickで発火する関数
    <>
      <Typography>{count}</Typography>
      <Button onClick={addCount}>count up</Button>
    </>
  );
};

このコンポーネントが本当に必要なパーツは、カウントされて画面に表示される値と、クリックしたときの関数です。
なので、このコンポーネントは、新しくカウントをセットするsetCountとカウントを増加させるという内部ロジックは不要と言えます。

今不要と言ったこのロジックを外に出し、隱蔽すると次のコードになる

カスタムフックにロジックを隱蔽

const CounterComponent = () => {
  const { count, countUp } = useCounter();
  return (
    <>
      <Typography>{count}</Typography>
      <Button onClick={countUp}>count up</Button>
    </>
  );
};

//デフォルトは+1される
const useCounter = (increment = 1) => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount((prev) => prev + increment);
  };

  return { count, countUp };
};

このようにすることで、「値を操作するhooks」と「その値をただ表示するだけのコンポーネント」と役割がはっきりするように見えます。
また、コードもスッキリするので気持ちもスッキリするといったところでしょうか))

カウンター以外の少し複雑な使い方は調べたら出てくるのでそちらを参考にしてください。

パフォーマンス

少し話がそれますが、これだけは頭に留めておいた方が良いことを以下に書きます。

カスタムフックはパフォーマンスの問題を引き起こしがち(らしい)ので、カスタムフックを作る際は常にuseMemoやuseCallbackの使用の検討をおすすめします。

このあたりの記事を読むと良いです。
https://dev.classmethod.jp/articles/react-custom-hooks/
https://blog.uhy.ooo/entry/2021-02-23/usecallback-custom-hooks/

カジュアルに使うモチベ

「ReactのカスタムHooksをカジュアルに使う」に以下が書いてありました

  • カスタムHooksというと、どちらかというとReactの中では難しい部類に入ります。主に「使い方がわからない」「公式ドキュメントが不親切」「ネットの解説が難しい」あたりが問題になるでしょう。しかし難しい機能だからと言って難しく使う必要はなく、自分の使える範囲で自由に使えばいいのではないかと思います。
  • カスタムHooksは一般にロジックの分離や再利用性の向上、テストの容易化などのために使われますが、実際にはもう少しカジュアルに使っても大丈夫です。難しいこと考えずにまずは「フックが増えすぎたからカスタムHooks導入してみよう」ぐらいの気持ちから始めて行きましょう。
  • カスタムHooksというと複雑な事情を備えた高度な機能だと捉えがちです。もちろん結合の緩和であるとか抽象的なインターフェイスの提供などに使うのが一番効果を発揮するのですが、まずはカジュアルに、ちょっとした処理をまとめるところから始めてみませんか?

この記事を読む前までは食わず嫌いのように勝手に難しく考えすぎて使うのを躊躇してました。
しかし、ここに書いてある通り難しいことは考えずまずはカウンター程度のhooksから切り出すことから始めると少しずつ理解出来てきます。

実際、カスタムフックを作ろうとすることで、ロジックの細分化を自然と考えるようになったことが個人的に良かったです。

カジュアルに、ちょっとした処理をまとめるところから始めてみませんか?
と言われたのでカジュアルに作った例を2つ紹介します

実際にカスタムフックを作ってみた

画面サイズを動的に取得するHook

/**
 * 画面サイズを動的に取得する
 */
export const useWindowSize = () => {
  const ref = useRef<NodeJS.Timeout | number>();
  const delay = 500;
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);

  const updateSize = useCallback(() => {
    clearTimeout(Number(ref.current));
    ref.current = setTimeout(() => {
      //When resizing the screen on the desktop, a delay of 500 ms is provided
      //because the setState event is fired every time.
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    }, delay);
  }, []);

  useLayoutEffect(() => {
    window.addEventListener('resize', updateSize);
    updateSize();
    return () => window.removeEventListener('resize', updateSize);
  }, [updateSize]);
  return { width, height };
};

ページタイトルを変更するHook

前提

  • 各ページでページタイトルが異なる
  • pageTitleはreduxで管理されている。

hooksを作らずに書くと

const ComponentA = () => {
  const dispatch = useAppDispatch()

  useEffect(() =>  {
dispatch(setPageTitle('ComponentAのページ'))
    } , [dispatch])

  return<div>A</div>
}
const ComponentB = () => {
  const dispatch = useAppDispatch()

  useEffect(() =>  {
dispatch(setPageTitle('ComponentBのページ'))
    } , [dispatch])

  return<div>B</div>
}

↑各コンポーネントでuseEffectやdispatchを呼び出すのは無駄な処理と言える。

Hooksに切り出すと

export const usePageTitle = (title = '') => {
  const dispatch = useAppDispatch(); //reduxのreducer
  useEffect(() => {
    dispatch(setPageTitle(title));
  }, [title, dispatch]);
};

const ComponentA = () => {
  usePageTitle('ComponentAのページ')
  return<div>A</div>
}

const ComponentB = () => {
  usePageTitle('ComponentBのページ')
  return<div>B</div>
}

Discussion