Reactのカスタムフックに入門する

Reactのカスタムフックに入門する

2025-01-18

今回はReactのカスタムフックに入門します。

実装例のリポジトリはこちらになります。

目次

  • 環境
  • そもそもReactのカスタムフックとは
  • カスタムフックの基本的な使い方: 入力値の状態管理
    • カスタムフック無しの入力値管理の実装
    • カスタムフックを使って、入力値管理をシンプルに
  • カスタムフックの利用例
    • 表示・非表示の切り替え
    • データ取得処理
    • ドラッグ&ドロップ処理
  • まとめ
  • 参考

環境

今回は以下の環境でテストしています。

  • macOS: Sonoma 14.2.1
  • Node.js: v22.12.0
  • npm: 10.9.0
  • Next.js: v15.1.5

そもそもReactのカスタムフックとは

ReactにはuseStateやuseEffectといったもともと用意されている組み込みフックがあります。

しかし組み込みフックだけではプロジェクト固有の複雑なロジックを複数のコンポーネントで再利用するのが難しい場合があります。

そんな時には独自のカスタムフックを作成することで、プロジェクト固有のロジックを複数コンポーネントで再利用することが可能になります。

具体例としてはAPIからデータを取得する処理や、フォームの入力値管理といったロジックをカスタムフックで共通化することができます。

またカスタムフックではフックごとにテストができるので、コードの安全性も増します。

注意点としてカスタムフックを命名する際はuseから始める必要があります。

次のセクションからは、カスタムフックを使った場合と使わなかった場合の実装を比較し、カスタムフックの利便性を解説します。

カスタムフックの基本的な使い方: 入力値の状態管理

このセクションでは良くある入力画面でのカスタムフックの利用例を紹介します。

カスタムフック無しの入力値管理の実装

まずはカスタムフック無しでの実装例です。

ファーストネームとラストネームの入力値をそれぞれuseStateを使って管理しています。入力時はそれぞれのhandleChange関数を用意して、Stateの値を更新しています。

'use client';
import { useState } from "react";

export default function Home() {

  const [ firstName, setFirstName ] = useState<string>('');
  const [ lastName, setLastName ] = useState<string>('');

  const handleFirstNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFirstName(e.target.value);
  };

  const handleLastNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setLastName(e.target.value);
  }

  return (
    <>
      First Name: <input
        value={firstName}
        onChange={handleFirstNameChange}
      /><br/><br/>
      Last Name: <input
        value={lastName}
        onChange={handleLastNameChange}
      />
    </>
  );
}

上記の例では入力値の状態を管理するuseStateと状態を変更するための関数がそれぞれ別で定義されており、冗長なコードとなっています。

例の場合は2つだけなのでそれほど問題とはなりませんが、入力項目が5個、10個と増えていった場合、各useStateとhandleChangeを定義するのは非効率です。

カスタムフックを利用することで、これらのロジックを1つにまとめることができます。

カスタムフックを使って、入力値管理をシンプルに

次にカスタムフックを使った場合、どのようにコードが統一され、再利用可能となるか確認します。

まずはuseInputというカスタムフックを作ります。

import { useState } from "react";

export const useInput = (): [ 
  string,
  (e: React.ChangeEvent<HTMLInputElement>) => void
] => {
  const [ value, setValue ] = useState<string>('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  return [ value, handleChange ];

}

useInput内でuseStateを使った状態管理と、その状態を更新するための関数を定義しています。そしてuseInputは値と更新するための関数を返却します。

次にコンポーネントでuseInputを使って入力欄を表示してみます。

'use client';
import { useInput } from "@/hooks/useInput";

export default function useFookPage() {

  const [ firstName, changeFirstName ] = useInput();
  const [ lastName, changeLastName ] = useInput();

  return (
    <>
      First Name: 
      <input
        value={firstName}
        onChange={changeFirstName}
      /><br/><br/>
      Last Name:
      <input
        value={lastName}
        onChange={changeLastName}
      />
    </>
  )
}

かなりコードがスッキリしていることがわかるかと思います。カスタムフックのuseInputを実行し、その戻り値を利用してコンポーネントを構築しています。

ロジックは全てuseInput内にまとまっていることになります。バリデーションやフォーカス管理などの機能を追加する際もuseInputを変更するだけで可能となります。

また応用して一つのinputだけでなく、フォーム全体を管理するような、useFormのようなカスタムフックを作っても良いかもしれません。

カスタムフックの利用例

ここからは3つのカスタムフックの利用例を紹介します。

  • 表示・非表示の切り替え(Toggle)
  • データ取得処理
  • ドラッグ&ドロップ処理

表示・非表示の切り替え

最初に表示・非表示の切り替えを行うためのカスタムフックです。

初期値をBooleanで受け取り、現在値と切り替えるための関数を返却します。

import { useState } from "react";

export const useVisible = (initialVisible: boolean): [
  boolean,
  () => void
] => {
  const [ visible, setVisible ] = useState<boolean>(initialVisible);

  const toggleVisible = () => {
    setVisible(!visible);
  }

  return [ visible, toggleVisible ];
};

上記のカスタムフックを利用する例は以下の通りです。

'use client';
import { useVisible } from "@/hooks/useVisible";

export default function VisiblePage() {

  const [ visible, toggleVisible ] = useVisible(true);

  return (
    <div>
      <h1>Visible Page</h1>
      <button onClick={toggleVisible}>Toggle Visibility</button>
      { visible && <p>Visible</p> }
    </div>
  );
}

データ取得処理

次にデータの取得処理をカスタムフックを使って実装します。

データの取得先は{JSON} Placeholderを利用しています。

import { useEffect, useState } from "react";

type FetchState<T> = {
  data: T | null;
  error: string | null;
  loading: boolean;
}

export const useFetch = <T>(url: string): FetchState<T> => {

  const [ data, setData ] = useState<T | null>(null);
  const [ loading, setLoading ] = useState<boolean>(true);
  const [ error, setError ] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);

        if (!response.ok) {
          throw new Error('fetch error');
        }

        const result: T = await response.json();
        setData(result);

      } catch (error) {
        setError((error as Error).message);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error};
}

上記のカスタムフックを利用する例はこちらです。

'use client';
import { useFetch } from "@/hooks/useFetch"

type Todo = {
  userId: number
  id: number
  title: string
  completed: boolean
}

export default function FetchPage() {

  const { data, loading, error } = useFetch<Todo[]>('https://jsonplaceholder.typicode.com/todos');

  if (loading) return <>Loading...</>
  if (error) return <>Error {error}</>

  console.log(data);

  return (
    <>
      <h1>Todo一覧</h1>
      {data?.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </>
  )
};

ドラッグ&ドロップ処理

最後はドラッグ&ドロップ処理を実装するカスタムフックです。

import { useState } from "react";

export const useDragAndDrop = <T>(initialItems: T[]) => {

  const [ items, setItems ] = useState<T[]>(initialItems);
  const [ draggingIndex, setDraggingIndex ] = useState<number | null>(null);

  const onDragStart = (index: number) => {
    setDraggingIndex(index);
  };

  const onDragOver = (index: number) => {
    if (draggingIndex === null || draggingIndex === index) return;

    const updateItems = [...items];
    const [draggedItem] = updateItems.splice(draggingIndex, 1);
    updateItems.splice(index, 0, draggedItem);

    setDraggingIndex(index);
    setItems(updateItems);
  };

  const onDragEnd = () => {
    setDraggingIndex(null);
  }

  return { items, onDragStart, onDragOver, onDragEnd };
};

上記のカスタムフックを利用する例はこちらです。

'use client';
import { useDragAndDrop } from "@/hooks/useDragAndDrop";

type Item = {
  id: number;
  name: string;
}

export default function DragAndDropPage() {

  const initialItems: Item[] = [
    { id: 1, name: 'Item1'},
    { id: 2, name: 'Item2'},
    { id: 3, name: 'Item3'},
    { id: 4, name: 'Item4'},
    { id: 5, name: 'Item5'},
    { id: 6, name: 'Item6'},
  ]

  const { items, onDragStart, onDragOver, onDragEnd } = useDragAndDrop<Item>(initialItems);

  return (
    <>
      <h1>Drag And Drop</h1>
      <ul>
        {items.map((item, index) => (
          <li
            key={item.id}
            draggable
            onDragStart={() => onDragStart(index)}
            onDragOver={(e) => {
              e.preventDefault();
              onDragOver(index);
            }}
            onDragEnd={onDragEnd}
          >{item.name}</li>
        ))}
      </ul>
    </>
  )
};

まとめ

今回の記事ではReactのカスタムフックについて、カスタムフックの基礎や実装例について解説しました。

カスタムフックを利用することで、コードの再利用性やテスト可能性が高まります。プロジェクト内でロジックが複数回登場する際は、カスタムフックの導入を検討してみてください。

参考