🎉

React Hooks でカスタムフックを作ってみよう

2021/04/02に公開

React Hooks とは

React 16.8 で追加された新機能が React Hooks です。
関数コンポーネント内でも、state や様々な React の機能を使うことができます。

React Hooks を使うと、基本的[1] に関数コンポーネントだけで実装ができます。

カスタムフックとは

フックは JavaScript の関数であり、カスタムフックは独自に作ったフックです。

フックを使うと以下のようなメリットがあります。

  • コンポーネント内で複雑になったロジックを分離できる
  • ロジックを再利用しやすくなる
  • テストが簡単になる

では、実際にカスタムフックを作ってみながら、メリットを体験してみましょう。

カスタムフックを作ってみる

いくつかフックを使うためにはルール(後述)がありますが、まずは作ってみましょう。

説明のための サンプルコード を用意しました。
ボタンをクリックする都度、数が加算されるカウンタの処理になっています。
早速、カスタムフックで書き直してみましょう。

import { useState } from "react";
import { DefaultButton } from "../../components/DefaultButton";
const Home = () => {
    const [count, setCount] = useState<number>(0);
    return (
        <div>
            <div>現在の値:{count}</div>
            <DefaultButton onClick={() => setCount((prevCount) => prevCount + 1)}>
               +1 する
            </DefaultButton>
        </div>
    );
};
export { Home };

カスタムフック(useCounter) を作る

カスタムフックを配備するディレクトリ構成に特にルールはありませんが、hook/index.tsx を作り配備します。

import { useState } from "react";
export const useCounter = () => {
    const [count, setCount] = useState(0);
    const increment = () => {
        setCount((prevCount) => prevCount + 1);
    };
    return { current: count, increment: increment };
};

これで useCounter が出来上がりです。

  • 現在のカウンタの値 current の値を取得できる
  • 現在のカウンタの値を +1 してくれる increment メソッドを利用できる

ということになります。

カスタムフック(useCounter) を使う

実際に画面内で使うシーンを見てみましょう。

import { useState } from "react";
import { DefaultButton } from "../../components/DefaultButton";
import { useCounter } from "../../hooks";
const Home = () => {
  const { current, increment } = useCounter();
  return (
    <div>
      <div>現在の値:{current}</div>
      <DefaultButton onClick={() => increment()}>+1 する</DefaultButton>
    </div>
  );
};
export { Home };

単純なコードでもカウンタ専用のカスタムフックを作ることで、コードが分かりやすくなったことにお気づきでしょうか。

  • useCounter とすることで useState を使うよりもカウンタの処理だということが明確になる
  • カウンタの値を +1 増やすという暗黙のルールを useCounter 内に持ち込むことができる
  • increment しか用意されていないので、勝手にカウンタの値を減らされる心配がない(笑)

といったところでしょうか。

つまり、「コンポーネントに内在していたカウンタという関心を分離できる」 ということですね。

今回画面内にあったカウンタの処理は、useCounter というカスタムフックになりました。
結果として、UI とロジックが分離できテストも書きやすくなります。

説明につかったサンプルコードは、こちら になります。

カスタムフックのルール

カスタムフックの概要が分かったところで、ルールを改めて確認してみましょう。
公式サイトにもある通り、カスタムフックには フックのルール というものがあります。

補足を加えながら見ていきましょう。

ルール1. フックを呼び出すのはトップレベルのみ

フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけません。代わりに、あなたの React の関数のトップレベルでのみ呼び出してください。これを守ることで、コンポーネントがレンダーされる際に毎回同じ順番で呼び出されるということが保証されます。これが、複数回 useState や useEffect が呼び出された場合でも React がフックの状態を正しく保持するための仕組みです

実際に試してみましょう。
先程のサンプルコードを少し書き換えたエラーが生じる サンプルコード を用意しました。

const Home = () => {
    const [count, setCount] = useState<number>(0);
    const callUseCounter = () => {
        // 関数内での呼び出し 🙅
        const {current} = useCounter();
        console.log(current);
    };
    return <div>
        <div>現在の値(useState):{count}</div>
        <DefaultButton onClick={() => setCount((prevCount) => prevCount + 1)}>
            +1 する
        </DefaultButton>
        <div>現在の値(useCounter):{current}</div>
        <DefaultButton onClick={() => increment()}>+1 する</DefaultButton>
    </div>
}

関数内で useCounter を呼び出したり

const Home = () => {
  const [count, setCount] = useState<number>(0);
  if (count > 10) {
    return (
      <div>
        <div>現在の値(useState):{count}</div>
        <DefaultButton onClick={() => setCount((prevCount) => prevCount + 1)}>
          +1 する
        </DefaultButton>
      </div>
    );
  } else {
    // 条件分岐内での呼び出し 🙅
    const { current, increment } = useCounter();
    return (
      <div>
        <div>現在の値(useState):{count}</div>
        <DefaultButton onClick={() => setCount((prevCount) => prevCount + 1)}>
          +1 する
        </DefaultButton>
        <div>現在の値(useCounter):{current}</div>
        <DefaultButton onClick={() => increment()}>+1 する</DefaultButton>
      </div>
    );
  }
};

条件分岐させて useCounter を利用させることはルール違反です。

ちなみにこのようなルール違反になるコードを書いた場合、
ESLint がルールを強制するために下記のような警告を出していることから分かります。

React Hook "useCounter" is called conditionally. 
React Hooks must be called in the exact same order in every component render.
Did you accidentally call a React Hook after an early return?(react-hooks/rules-of-hooks)

なぜ同じ順番である必要があるのか

useState のコードを見ると分かりやすいです。

  const [name, setName] = useState('名前');
  const [address, setAddress] = useState('住所');
  const [age, setAge] = useState('年齢');

たとえば、 3つ の state があるとします。
React は useState の呼び出しが 名前 なのか 住所 なのか 年齢 なのか。
どうやって判断するのでしょうか。

「React はフックが呼ばれる順番に依存している」(公式抜粋) のです。

フックが呼ばれる順番が一定だとローカル内に state を割り当てることができます。

だから 「同じ順番で呼び出さないといけない」 ということになり、
結果として、「フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけません。」 という説明に繋がります。

ルール2. 関数コンポーネント もしくは カスタムフック内 から呼び出す

ルール3. "use" で始まり直後が大文字である関数であること

export const usehello = () => {
  // usehello は 🙅
  // useHello は 🙆
  const [name] = useState("taro");
  return name;
};
export const hoge = () => {
  // hoge は 🙅
  // useHoge は 🙆
  const [name] = useState("hoge");
  return name;
};

サンプルコード から確認ができます。

ESLintusehellohoge メソッドに対して、ルールを強制するために下記のような警告を出していることからも分かります。

React Hook "useState" is called in function "usehello" that is neither a React function component nor a custom React Hook function. 
React component names must start with an uppercase letter. (react-hooks/rules-of-hooks)

もっとカスタムフックに触れてみよう

カスタムフックに関する awesome リスト(特定テーマに関するキュレーションを行うリポジトリ)を紹介します。

awesome リストでも紹介されていますが、react-use というカスタムフック集もコードの読み物としておすすめです。

最後に

いかがでしたでしょうか。

カスタムフックを使うと複雑になりがちな巨大なコンポーネントも関心を分離できて、見通しの良いコードになります。まだ一度も使ったことのない方は、簡単なところから使ってみましょう。

この記事が気に入った方は、下のハートマークを押してもらえると嬉しいです。

脚注
  1. フックはクラスのユースケースのすべてをカバーしていますか? ↩︎

Discussion