導入・背景
ReactアプリケーションでAPI通信を行う際、useEffect
を使って非同期処理を記述することが一般的です。しかし、毎回useEffect
の中にfetch
やaxios
を利用してAPI通信を記述すると、コードが冗長になり、コンポーネントの可読性が低下します。
そこで、API通信処理を効率的に管理するためにカスタムフック usePromise
を作成し、データ取得の状態(loading
、data
、error
)をシンプルに扱えるようにしました。本記事では、このusePromise
の実装方法、利用ケース、テストの書き方まで詳しく解説します。
関連パッケージインストール
usePromise
を実装するにあたり、必要なパッケージは以下の通りです。基本的にReactとTypeScriptのみで動作します。
必須パッケージ
- React:
react
およびreact-dom
- TypeScript: 型定義を追加するため
以下のコマンドでセットアップできます。
npm install react react-dom
npm install --save-dev typescript @types/react @types/react-dom
実行コード
import { useEffect, useState } from "react";
type FetchState<T> = {
data: T | null;
loading: boolean;
error: Error | null;
};
function usePromise<T>(
requestCommand: (...args: any[]) => Promise<T>,
args?: object
): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
setState({ data: null, loading: true, error: null });
try {
const response = await requestCommand(args);
if (!isCancelled) {
setState({ data: response, loading: false, error: null });
}
} catch (err) {
if (!isCancelled) {
setState({ data: null, loading: false, error: err as Error });
}
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [requestCommand, args]);
return state;
}
export default usePromise;
Jestを使ったテストコード
usePromise
のテストにはJestとReact Testing Libraryを使用します。
パッケージのインストール
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
テストコード:usePromise.test.tsx
import { renderHook } from "@testing-library/react";
import usePromise from "./usePromise";
describe("usePromise", () => {
const mockApi = jest.fn();
it("should return data when the request is successful", async () => {
mockApi.mockResolvedValue({ message: "Success" });
const { result, waitForNextUpdate } = renderHook(() =>
usePromise(mockApi)
);
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toEqual({ message: "Success" });
expect(result.current.error).toBeNull();
});
it("should return error when the request fails", async () => {
mockApi.mockRejectedValue(new Error("Fetch failed"));
const { result, waitForNextUpdate } = renderHook(() =>
usePromise(mockApi)
);
expect(result.current.loading).toBeTruthy();
await waitForNextUpdate();
expect(result.current.loading).toBeFalsy();
expect(result.current.error).toEqual(new Error("Fetch failed"));
expect(result.current.data).toBeNull();
});
});
利用ケース
1. API通信を行う場合
usePromise
を使えば、シンプルにAPIリクエストとその状態管理ができます。
import usePromise from "./usePromise";
import axios from "axios";
const fetchUserData = () => axios.get("https://jsonplaceholder.typicode.com/users").then(res => res.data);
const UserComponent = () => {
const { data, loading, error } = usePromise(fetchUserData);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>User Data</h1>
{data.map((user: any) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
export default UserComponent;
2. 引数を使ってAPI通信をする場合
API関数に引数を渡す場合にも対応しています。
const fetchUserById = (params: { id: number }) =>
axios.get(`https://jsonplaceholder.typicode.com/users/${params.id}`).then(res => res.data);
const UserDetail = ({ userId }: { userId: number }) => {
const { data, loading, error } = usePromise(fetchUserById, { id: userId });
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>User Detail</h1>
<p>Name: {data.name}</p>
</div>
);
};
まとめ
usePromise
を使うことで、API通信におけるデータの取得・エラーハンドリング・ローディング状態の管理をシンプルに行うことができます。これにより、Reactコンポーネントの責務を減らし、可読性・保守性が向上します。
本記事のポイント
usePromise
の実装方法- Jestを使ったテストコード
- API通信の利用ケース
このカスタムフックはどのReactプロジェクトにも容易に統合でき、DRY(Don’t Repeat Yourself)の原則を守りつつ、コードを効率化します。
コメント