1. はじめに
Reactの学習を進めていると、コンポーネントの中にデータ取得・ローディング管理・エラー処理がどんどん増えて読みにくくなっていく場面に遭遇しました。カスタムフックはそのロジック部分を別ファイルに切り出して、コンポーネントをUIの描画だけに専念させるための仕組みのようです。今回は useAllUsers というカスタムフックを自作しながら学んだ内容を整理しました。
2. カスタムフックの基本
カスタムフックは、useState や useEffect などの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 から useState や axios の記述が完全に消え、「どんなUIを表示するか」だけが書かれたファイルになりました。
4. カスタムフックにするメリット
4.1 コンポーネントの役割が明確になる
カスタムフックに切り出すことで、「このコンポーネントはUIを描画するファイル」「このフックはデータを取得するファイル」という責務の分離ができると理解しました。
4.2 同じロジックを複数のコンポーネントで使い回せる
たとえば UserListPage と UserSearchPage の両方でユーザー取得が必要な場合、useAllUsers() を呼び出すだけで同じロジックを再利用できます。
// 別のコンポーネントでも同じフックをそのまま使える
const { getUsers, userProfiles, loading, error } = useAllUsers();
同じロジックをコンポーネントごとにコピーすると、修正が必要なときに複数箇所を直さないといけなくなります。カスタムフックにまとめておくと変更箇所が1ファイルだけで済むと理解しました。
まとめ
今回の気づき
カスタムフックを作る前は「コンポーネントにロジックを書くのが普通」と思っていましたが、「コンポーネントはUIの描画だけに専念させる」という考え方を実際のコードで体感できました。use から始まる関数を作るだけというシンプルなルールで責務を分けられるのは、想像よりずっとやりやすかったです。
ハマりやすいポイント
- 関数名を
useから始めないとReactにフックとして認識されないみたいです -
.finally()を忘れると、成功・失敗どちらのケースでもloadingがtrueのままになってしまいます -
returnで返し忘れた値は、呼び出し側でundefinedになるので注意が必要でした