【React】カスタムフックの概要・メリット・使いどころ

JavaScript

カスタムフック(Custom Hooks)について

カスタムフックとはデフォルトのフックを利用して実装された関数です。

カスタムフックを実装する際のルール

カスタムフックはコンポーネントに実装されたロジックを取り出したものにすぎないので、カスタムフックの実装に条件や制約はありません。しかし、暗黙的なルールがいくつか存在します。

カスタムフック作成時のルール
  • メソッド名はuseを接頭辞にする
  • カスタムフック内ではデフォルトのフックを利用する
  • 戻り値のパターンはデフォルトのフックに従う

上記のルールを無視したカスタムフックは開発者の混乱をまねく原因になるため、原則ルールに従ったカスタムフックを作成します。

以下では各ルールについて補足説明をします。

メソッド名はuseを接頭辞にする

フックであることがメソッド名からわかるようにするため、デフォルトのフックと同じくuseを接頭辞にした命名にします。

カスタムフック内ではデフォルトのフックを利用する

カスタムフックの実装に制約はないため、以下のメソッドもカスタムフックといえます。

あまりよくないカスタムフックの例

const useHello = () => {
  return 'Hello';
};

しかしデフォルトのフックを使いやすくしたり拡張したりするのがカスタムフックの目的です。
ですので、カスタムフックではデフォルトのフックを利用するのが一般的です。

戻り値のパターンはデフォルトのフックに従う

戻り値はデフォルトのフックに従い、以下の3つのいずれかのパターンにするとよいです。

カスタムフックの戻り値のパターン
  • なし(useEffectパターン)
  • 1つ(useRef・useContextパターン)
  • 要素が2つの配列(useState・useReducerパターン)

戻り値が多すぎる場合は責務分割がで適切にできていない可能性があるため、カスタムフックをさらに分解することを検討するとよいです。

カスタムフックのメリット

カスタムフックのメリット
  • コンポーネントのコードが簡潔になる
  • 関連する複数のフックを1つの関数にまとめられる
  • フックの共通化ができる

以下ではそれぞれのメリットについて解説します。

コンポーネントのコードが簡潔になる

以下は『API経由で取得したTodoリストの内容を画面に表示する』例です。

以下のコードにはTodoリストに関する実装(useStateuseEffect)が含まれており、コンポーネントの可読性がよくありません。

App.tsx

import axios from "axios";
import { useEffect, useState } from "react";

type TodoItem = {
  id: number;
  content: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = useState([] as TodoItem[]);
  useEffect(() => {
    const getTodoRequest = async () => {
      const response = await axios.get("http://localhost:4000/todos");
      const todos = response.data;
      return todos;
    };
    getTodoRequest().then((todos) => setTodos(todos as TodoItem[]));
  }, []);
  return (
    <>
      <h2>TodoList</h2>
      <ul>
        {todos.map(({ id, content }) => (
          <li key={id}>
            <div>{content}</div>
          </li>
        ))}
      </ul>
    </>
  );
};
export default App;

カスタムフックを利用すると以下のようになります。

src/lib/hooks/useTodos.ts

import axios from "axios";
import { useEffect, useState } from "react";

type TodoItem = {
  id: number;
  content: string;
  completed: boolean;
};

const useTodos = () => {
  const [todos, setTodos] = useState([] as TodoItem[]);
  useEffect(() => {
    const getTodoRequest = async () => {
      const response = await axios.get("http://localhost:4000/todos");
      const todos = response.data;
      return todos;
    };
    getTodoRequest().then((todos) => setTodos(todos as TodoItem[]));
  }, []);
  return todos;
};
export default useTodos;

App.tsx

import useTodos from "./lib/hooks/useTodos";

const App = () => {
  const todos = useTodos();
  return (
    <>
      <h2>TodoList</h2>
      <ul>
        {todos.map(({ id, content }) => (
          <li key={id}>
            <div>{content}</div>
          </li>
        ))}
      </ul>
    </>
  );
};
export default App;

上記のカスタムフック適用後のコードを見て分かる通り、Todoリスト取得に関するロジックがコンポーネントからカスタムフックに移動し、コンポーネントのコードが簡潔になったことがわかります。

上記の例はあくまでカスタムフックを理解するためのサンプルコードです。
API経由のデータ取得は例えばSWRを利用する方法などもあります。

関連する複数のフックを1つの関数にまとめられる

コンポーネントに直接フックを実装する場合、フックが増えた際にフックの関連性がわかりにくくなります。

しかし、先ほど紹介したカスタムフック(useTodos.ts)をみてわかる通り、Todoリストに関するフック(useStateuseEffect)はTodoリストのカスタムフック内にまとめられます。

カスタムフックを導入することにより、コンポーネント側はカスタムフックを呼ぶだけで関連するフックを意識することなくデータが扱えます。

フックの共通化ができる

複数のコンポーネントで『API経由でTodoリストを取得する』という実装をする必要があったとします。
この時、先ほど紹介したカスタムフック(useTodos.ts)が用意されていれば、コンポーネントはデータ取得のロジックを実装せずともカスタムフックを呼び出すだけでよくなります。

複数の場所で同じようなフックを実装する場合はカスタムフックを利用することでコードの共通化が図れます。

カスタムフックのパターン

カスタムフックのパターンはいくつかありますが、今回は2つ紹介します。

カスタムフックのパターン
  • 戻り値がデータ(State)のみ
  • 戻り値がデータ(State)とデータ更新をする関数

以下ではそれぞれのパターンについて説明します。

戻り値がデータ(State)のみ

データ取得や作成に関するロジックをコンポーネントからカスタムフックに移動させたいときのパターンです。
カスタムフックにデータの処理を実装することで、コンポーネントはカスタムフックを呼び出すだけで加工済みのデータを扱えます。

このパターンの場合、Stateの更新はカスタムフック内で行われます。
ですので、コンポーネントから意図しないデータ更新が発生しなくなり安全にデータを扱えるという副次的なメリットもあります。

先ほど紹介したTodoリストのカスタムフックはこのパターンです。
他にもReactのカスタムHooksをカジュアルに使ってコードの見通しを良くしようで紹介されているカスタムフックや、React『独自フックの作成』で紹介されているカスタムフックもこのパターンに該当します。

戻り値がデータ(State)とデータ更新をする関数

コンポーネントにStateを更新するトリガ(関数)が存在しているが、更新のロジックはカスタムフックに移動させたいときのパターンです。

具体例を紹介します。以下は『ボタンをクリックするとカウントが増える』例です。

App.tsx

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount((prevCount) => prevCount + 1);
  };
  const handleClick = () => {
    countUp();
  };
  return (
    <>
      <div>{count}</div>
      <button onClick={handleClick}>+Count</button>
    </>
  );
};
export default App;

カスタムフックを利用すると以下のようになります。

src/lib/hooks/useCount.ts

import { useState } from "react";

const useCount = () => {
  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount((prevCount) => prevCount + 1);
  };
  return [count, countUp] as const;
};
export default useCount;

App.tsx

import useCount from "./lib/hooks/useCount";
import "./styles.css";

const App = () => {
  const [count, countUp] = useCount();

  const handleClick = () => {
    countUp();
  };
  return (
    <>
      <div>{count}</div>
      <button onClick={handleClick}>+Count</button>
    </>
  );
};
export default App;

上記のカスタムフック適用後のコードを見て分かる通り、State(コードでいうcount)の更新処理(コードでいうcountUp())がコンポーネントからカスタムフックへ移動したことにより、コンポーネントのコードが簡潔になります。

カスタムフックを呼び出すと『State』と『Stateを更新する関数』が取得できるため、コンポーネントはStateの利用箇所と更新のトリガを気にするだけでよくなります。

useCallbackはとにかく使え! 特にカスタムフックではで紹介されているtoggle関数のカスタムフックもこのパターンに該当します。

なお、useCallbackはとにかく使え! 特にカスタムフックではで紹介されている通り、カスタムフックの戻り値の関数には、メモ化されたコンポーネントのPropsに渡される場合を考慮してuseCallbackを適用しておくとよいです。

コンポーネントのメモ化(React.memo, useMemo)およびuseCallbackの詳細解説は具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニングで紹介しています。

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料