React カスタムフックの基本と実務での活用例をやさしく解説

はじめに

Reactで開発を進めていくうちに、同じような処理を何度も書く場面が出てきませんか。 一度実装したロジックを別のコンポーネントでも使い回したいと思うこともあるでしょう。 そういった重複を減らし、整理されたコードを書くためにカスタムフックという仕組みがあります。 カスタムフックは、既存のフック(useStateuseEffectなど)の組み合わせや、共通処理をひとまとめにする方法です。 うまく活用することで、コードを読みやすくしながら保守性を高めることもできます。 今回は、そんなカスタムフックの基本と、実務で想定される活用例をやさしく紹介していきます。

この記事を読むとわかること

  • カスタムフックの役割とメリット
  • カスタムフックの作り方
  • 実務で想定するような具体例
  • カスタムフックを使用するときの注意点

カスタムフックの基本

カスタムフックは、Reactのフックを使って独自のロジックをまとめた関数のことです。 名前を**use**から始めるルールがあり、たとえばuseFetchDatauseFormInputといった形にすると、Reactで定義されたフックと同様に扱えます。 この仕組みを活用すると、コンポーネント内で繰り返し使うロジックをひとまとめにできるので、一度書いたコードを多くのコンポーネントで再利用できます。

また、カスタムフックを使うときは、一般的に以下のことを意識します。

  • どういったロジックを切り出すか
  • どのような引数や戻り値が必要か
  • 状態や副作用の管理をどう行うか

複雑なコンポーネントも、見通しの良いコード構造にできることがポイントです。 さらにロジックをカスタムフックとして隔離することで、コンポーネントの表示ロジックとビジネスロジックを分ける感覚を持ちやすくなります。

カスタムフックの役割

カスタムフックを使う最大の理由は、状態管理や副作用を伴うロジックを再利用するためです。 もし、あるロジックが複数のコンポーネントで必要だとわかったら、そこをカスタムフックにまとめておけば、使い回しが可能になります。

たとえば、フォーム入力のバリデーションや、APIからデータを取得してステートを更新するといった処理は、コンポーネントごとに書いてしまうと煩雑です。 しかし、カスタムフックとして切り出しておけば、単純にそのフックを呼び出すだけで同じ処理を何度でも行えます。 また、ロジックが集中している場所を明確にできるので、後で修正が必要になったときも1か所を変更すればよい形にしやすいです。

なぜカスタムフックを使うのか

ReactにはもともとuseStateuseEffectなどのフックが用意されていますが、特定の利用シーンに合わせた処理をまとめられると、プロジェクト全体の可読性が上がります。 複数のコンポーネントで同じような処理をする場合でも、そのロジックをどう組み合わせたらよいかをカスタムフックという形でカプセル化できるからです。 さらにカスタムフックはシンプルな関数なので、引数と戻り値によって柔軟なカスタマイズを行えます。 コンポーネントごとに微妙に違う部分は引数で補い、同じ部分は共通化するイメージを持つとわかりやすいでしょう。

カスタムフックの作り方

実際にカスタムフックをどのように作るか見ていきましょう。 前提として、カスタムフックは通常の関数と似ていますが、内部でReactのフックを呼び出す点が特徴です。 呼び出し時にuseStateuseEffectを使い、その結果を戻り値として返すことで、呼び出し元のコンポーネントに必要なデータや関数を提供できます。

シンプルな例:フォームの入力管理

まずは、フォーム入力を管理するカスタムフックの例を考えてみます。 複数の入力フィールドを扱う際、状況によっては同じようなステート管理コードを何度も書いてしまうことがあります。 そこで以下のように、入力値と更新用の関数をまとめたカスタムフックを用意すると便利です。

import { useState } from "react";

export function useFormInput(initialValue = "") {
  const [value, setValue] = useState(initialValue);

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return {
    value,
    onChange: handleChange
  };
}

このuseFormInputというフックを作ると、フォームを扱うコンポーネントでは次のように使えます。

import React from "react";
import { useFormInput } from "./useFormInput";

function ExampleForm() {
  const username = useFormInput("");
  const email = useFormInput("");

  const handleSubmit = () => {
    console.log(username.value, email.value);
    // 何か登録処理を呼び出すなどの実装が考えられます
  };

  return (
    <div>
      <input type="text" {...username} placeholder="ユーザー名" />
      <input type="email" {...email} placeholder="メールアドレス" />
      <button onClick={handleSubmit}>登録</button>
    </div>
  );
}

export default ExampleForm;

コンポーネント内では、usernameemailという変数に入力値とイベントハンドラが返ってきます。 フォームの入力制御がひとつのカスタムフックにまとまっているため、コードの繰り返しを減らせるのが特徴です。

実務での応用例:データ取得処理

次に、実務でとくに需要の高いAPIからのデータ取得ロジックをカスタムフック化する例を見てみましょう。 データを取得する際には、リクエストの状態管理やエラー処理を行う必要があります。 この部分をコンポーネントに直接書いてしまうと、コードが長くなって読みづらくなりますよね。 そこでカスタムフックを使うと、データ取得のロジックをひとつにまとめられます。

import { useState, useEffect } from "react";

export function useFetchData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("データの取得に失敗しました");
        }
        const result = await response.json();
        if (isMounted) {
          setData(result);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // クリーンアップ関数
    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

そしてコンポーネントでは、このフックを呼び出すだけでデータ取得と状態管理が完結します。

import React from "react";
import { useFetchData } from "./useFetchData";

function UserList() {
  const { data: users, loading, error } = useFetchData("https://api.example.com/users");

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラーが発生しました: {error}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

このように、ロジックをカスタムフックとして切り出してしまえば、データ取得の流れをシンプルにできます。 また、別のURLに対して同じ処理が必要になった場合も、フックを再利用してURLだけ変えれば対応可能です。

カスタムフックで気をつけたいポイント

カスタムフックは便利ですが、いくつか注意点もあります。 たとえば、フックの実行順序がコンポーネントのレンダーサイクルに依存しているため、if文やループの中で呼び出さないといったルールがあります。 これは公式ドキュメントなどで「フックはトップレベルでのみ呼び出す」と説明されているところですね。

カスタムフックを含めて、Reactのフック全般を使う場合は「コンポーネント(またはカスタムフック)のトップレベルで呼び出すこと」が基本ルールです。

また、複雑なロジックを詰め込みすぎてしまうと逆に読みにくくなります。 使う側が簡単に呼び出せるよう、引数や戻り値の形はできるだけシンプルに設計するとわかりやすいです。

カスタムフックをメンテナンスしやすくするには、ユニットごとに分けて作るのがおすすめです。 たとえばデータの取得とステート管理を別々のフックに切り出すなど、同じ処理でも役割が異なる場合は分割しておくと保守がしやすくなります。

カスタムフックはReactが提供する公式フックを内側で使っているだけですが、使い勝手が悪いと感じたら、いったん関数の粒度を見直してみるとよいでしょう。

まとめ

ここまで、Reactのカスタムフックについて基本的な考え方と作り方、そして実務で想定される例を紹介しました。 カスタムフックは、コンポーネント内で重複する処理を整理し、プロジェクト全体のコードをより読みやすくするための手段といえます。 フォームやデータ取得のように実装頻度が高い機能は、カスタムフックにまとめると管理が楽になるでしょう。 ぜひ皆さんのプロジェクトでも、カスタムフックを活用して快適なReact開発を実践してみてください。

Reactをマスターしよう

この記事で学んだReactの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。