3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactのカスタムフック — コンポーネントからロジックを切り出して再利用可能にする

3
Posted at

1. はじめに

Reactの学習を進めていると、コンポーネントの中にデータ取得・ローディング管理・エラー処理がどんどん増えて読みにくくなっていく場面に遭遇しました。カスタムフックはそのロジック部分を別ファイルに切り出して、コンポーネントをUIの描画だけに専念させるための仕組みのようです。今回は useAllUsers というカスタムフックを自作しながら学んだ内容を整理しました。

2. カスタムフックの基本

カスタムフックは、useStateuseEffect などのReact組み込みフックを組み合わせた、再利用可能な関数のことのようです。

2.1 ルールは「useから始める」だけ

カスタムフックを作るうえで最低限のルールは、関数名を use から始めることです。これだけでReactがフックとして認識してくれると理解しました。

// ✅ useから始まればカスタムフックとして認識される
export const useAllUsers = () => { ... };

// ❌ useから始まっていないとフックのルールが適用されない
export const getAllUsers = () => { ... };

フック内で useState などを使う場合、use から始まる名前でないとReactのルール違反として警告が出ることがあります。

2.2 カスタムフックの基本的な構造

カスタムフックは「状態を持ち、関数を定義して、まとめて返す」という流れで作れます。

// カスタムフックの基本パターン
export const useXxx = () => {
  // 状態を定義する
  const [data, setData] = useState(...);

  // ロジックを関数として定義する
  const doSomething = () => { ... };

  // 呼び出し側が使う値と関数を返す
  return { data, doSomething };
};

呼び出し側は return で返したものを分割代入で受け取るだけになります。

3. 実装例 — useAllUsers

実際に作った useAllUsers で、カスタムフックの動きを確認します。

3.1 作成前 — ロジックがコンポーネントに混在している状態

フック作成前は、データ取得のロジックがすべて App.tsx の中にあります。

// データ取得・ローディング・エラーがすべてコンポーネント内に混在している
function App() {
  // 3つのstateを直接コンポーネントで管理している
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const getUsers = () => {
    setLoading(true);
    setError(false);
    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`,
        }));
        setUserProfiles(data);
      })
      .catch(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  return (
    <div className="App">
      <button onClick={getUsers}>データ取得</button>
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>Loading...</p>
      ) : (
        userProfiles.map((user) => <UserCard key={user.id} user={user} />)
      )}
    </div>
  );
}

App が「データを取る責務」と「UIを描画する責務」を両方持ってしまっているのが問題だと理解しました。

3.2 カスタムフックに切り出す

データ取得のロジック全体を useAllUsers.ts に移します。

// axiosでユーザー一覧を取得・整形するカスタムフック
import { useState } from "react";
import type { UserProfile } from "../types/userProfile";
import type { User } from "../types/api/user";
import axios from "axios";

export const useAllUsers = () => {
  // 3つのstateをフック内で管理する
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const getUsers = () => {
    setLoading(true);
    setError(false);
    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        // APIレスポンスを表示用の形に整形する
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`,
        }));
        setUserProfiles(data);
      })
      .catch(() => {
        // 失敗時はerrorフラグをtrueにしてUIに伝える
        setError(true);
      })
      .finally(() => {
        // 成功・失敗どちらでもloadingをfalseに戻す
        setLoading(false);
      });
  };

  // 呼び出し側が必要なものだけを返す
  return { getUsers, userProfiles, loading, error };
};

3.3 作成後 — App.tsxがUIだけになる

// フックを呼び出すだけになり、コンポーネントがUIに専念できる
import { UserCard } from "./components/UserCard";
import { useAllUsers } from "./hooks/useAllUsers";

function App() {
  // フックから値と関数を分割代入で受け取るだけ
  const { getUsers, userProfiles, loading, error } = useAllUsers();
  const onClickFetchUser = () => getUsers();

  return (
    <div className="App">
      <button onClick={onClickFetchUser}>データ取得</button>
      <br />
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>Loading...</p>
      ) : (
        userProfiles.map((user) => <UserCard key={user.id} user={user} />)
      )}
    </div>
  );
}

export default App;

App.tsx から useStateaxios の記述が完全に消え、「どんなUIを表示するか」だけが書かれたファイルになりました。

4. カスタムフックにするメリット

4.1 コンポーネントの役割が明確になる

カスタムフックに切り出すことで、「このコンポーネントはUIを描画するファイル」「このフックはデータを取得するファイル」という責務の分離ができると理解しました。

4.2 同じロジックを複数のコンポーネントで使い回せる

たとえば UserListPageUserSearchPage の両方でユーザー取得が必要な場合、useAllUsers() を呼び出すだけで同じロジックを再利用できます。

// 別のコンポーネントでも同じフックをそのまま使える
const { getUsers, userProfiles, loading, error } = useAllUsers();

同じロジックをコンポーネントごとにコピーすると、修正が必要なときに複数箇所を直さないといけなくなります。カスタムフックにまとめておくと変更箇所が1ファイルだけで済むと理解しました。

まとめ

今回の気づき

カスタムフックを作る前は「コンポーネントにロジックを書くのが普通」と思っていましたが、「コンポーネントはUIの描画だけに専念させる」という考え方を実際のコードで体感できました。use から始まる関数を作るだけというシンプルなルールで責務を分けられるのは、想像よりずっとやりやすかったです。

ハマりやすいポイント

  • 関数名を use から始めないとReactにフックとして認識されないみたいです
  • .finally() を忘れると、成功・失敗どちらのケースでも loadingtrue のままになってしまいます
  • return で返し忘れた値は、呼び出し側で undefined になるので注意が必要でした
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?