Reactで非同期データをTypeScriptで型安全に扱う方法

Reactアプリケーションの開発では、非同期データフェッチが不可欠な場面が多々あります。たとえば、APIからのデータ取得や外部サービスとの通信を扱う際には、非同期処理を適切に管理することが重要です。その一方で、動的型付けのみでは、予期しないエラーや予測困難な挙動が生じやすくなります。ここでTypeScriptを利用することで、データの型を明確に定義し、開発プロセスを効率化しながら信頼性を向上させることが可能です。本記事では、Reactにおける非同期データフェッチにTypeScriptを組み合わせることで得られる利点や具体的な実践方法について解説します。

目次
  1. 非同期データフェッチの基本概要
    1. Reactでの非同期処理の流れ
    2. 基本的なコード例
  2. TypeScriptを使用する利点
    1. 1. 型による明確なデータ構造の定義
    2. 2. 型推論による開発効率の向上
    3. 3. 型安全なエラーハンドリング
    4. 4. 開発時の補完とドキュメンテーションの強化
    5. 5. チーム開発における信頼性の向上
  3. 型定義の基本と実例
    1. 基本的な型定義
    2. 型定義を使用した非同期処理
    3. 型定義をReactで活用する
    4. APIレスポンスが不確定な場合の型定義
  4. Reactクエリとの組み合わせ
    1. Reactクエリの基本概要
    2. Reactクエリのセットアップ
    3. 非同期データフェッチと型付け
    4. キャッシュ管理と再利用性
    5. TypeScriptの利点を最大限に活用
  5. AxiosやFetch APIの利用時の型付け
    1. Axiosの型付け
    2. Fetch APIの型付け
    3. AxiosとFetch APIの使い分け
  6. 型安全なエラーハンドリングの実装
    1. 基本的なエラーハンドリング
    2. Reactコンポーネントでのエラーハンドリング
    3. カスタムエラー型を用いたエラーハンドリング
    4. Reactクエリとの統合
    5. エラーハンドリングのベストプラクティス
  7. 実践例:ユーザーリストのフェッチと表示
    1. APIレスポンスの型定義
    2. データフェッチ関数
    3. Reactコンポーネントでの表示
    4. コードの解説
    5. 最適化:カスタムフックの活用
    6. 結果
  8. 応用:カスタムフックを使った再利用可能な非同期処理
    1. 汎用的な非同期処理カスタムフックの設計
    2. カスタムフックの使用例
    3. 汎用カスタムフックの利点
    4. カスタムフックの拡張
  9. まとめ

非同期データフェッチの基本概要


非同期データフェッチは、Reactアプリケーションで外部リソースからデータを取得する際に頻繁に使用されます。通常、fetch APIやAxiosライブラリを使用して、バックエンドサーバーや外部APIから必要な情報を取得します。

Reactでの非同期処理の流れ


Reactでは非同期処理を扱う際に主に以下の手順を踏みます。

  1. コンポーネントがマウントされたタイミングでデータを取得
    データフェッチは通常、useEffectフックを使用して、コンポーネントがレンダリングされた後に行います。
  2. 状態管理でデータを保持
    取得したデータをコンポーネント内でuseStateフックを利用して保存します。これにより、データが更新されたときに自動的にUIが再描画されます。
  3. ローディング状態とエラーハンドリング
    データフェッチ中のローディング表示やエラー発生時の処理を適切に管理します。

基本的なコード例


以下に、Reactで非同期データフェッチを行う基本的なコードを示します。

import React, { useEffect, useState } from "react";

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

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => {
        if (!response.ok) {
          throw new Error("Network response was not ok");
        }
        return response.json();
      })
      .then((data) => {
        setData(data);
        setLoading(false);
      })
      .catch((error) => {
        setError(error.message);
        setLoading(false);
      });
  }, []);

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

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default App;

このように非同期処理を適切に管理することで、スムーズなユーザー体験を提供できます。この基礎を押さえた上で、次章ではTypeScriptを利用して型安全性を向上させる方法を解説します。

TypeScriptを使用する利点

非同期データフェッチにおいてTypeScriptを活用することで、コードの可読性や信頼性が飛躍的に向上します。特に、APIレスポンスの構造が複雑になる場合やチームでの開発において、型付けは重要な役割を果たします。以下に、TypeScriptを使用する主な利点を紹介します。

1. 型による明確なデータ構造の定義


TypeScriptを使用することで、APIレスポンスや関数の戻り値などの型を明確に定義できます。これにより、開発者はデータの形状を直感的に理解しやすくなり、予期しないエラーを減らすことが可能です。

type Post = {
  id: number;
  title: string;
  body: string;
};

このように型を定義しておくと、フェッチしたデータの型が自動的に検証され、予期せぬエラーを防げます。

2. 型推論による開発効率の向上


TypeScriptは型推論機能を備えており、明示的に型を指定しなくても、コンパイラがデータの型を推測してくれます。これにより、コード量を最小限に抑えつつ、型安全性を確保できます。

3. 型安全なエラーハンドリング


非同期処理では、エラーが発生する可能性が常にあります。TypeScriptを利用すれば、エラーオブジェクトや戻り値の型を適切に定義し、安全にエラーハンドリングを行うことができます。

async function fetchData(): Promise<Post[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    throw new Error("Failed to fetch data");
  }
  return response.json();
}

4. 開発時の補完とドキュメンテーションの強化


型情報はIDEの補完機能を強化し、開発者が関数やデータの使用方法を直感的に理解できるようにします。これにより、コードレビューやバグ修正が効率的になります。

5. チーム開発における信頼性の向上


明確な型付けは、コードの一貫性を保つだけでなく、他の開発者がコードを理解しやすくする助けとなります。これにより、プロジェクト全体の保守性が向上します。

TypeScriptを活用することで、非同期データフェッチの信頼性と可読性を向上させ、Reactアプリケーションの開発体験を大幅に改善できます。次章では、具体的な型定義の方法について解説します。

型定義の基本と実例

非同期データフェッチを型安全に行うためには、データの型を正確に定義することが重要です。ここでは、APIレスポンスに対して適切な型定義を作成する方法と、その具体例を解説します。

基本的な型定義


型定義は、APIから取得するデータの構造に基づいて作成します。たとえば、以下のようなJSONレスポンスを返すAPIを考えます。

JSONレスポンス例:

{
  "id": 1,
  "title": "TypeScript and React",
  "body": "This is a blog post about TypeScript and React."
}

このデータに対応する型をTypeScriptで定義すると、以下のようになります。

type Post = {
  id: number;
  title: string;
  body: string;
};

この型定義を使うことで、フェッチしたデータの正確性を保証できます。

型定義を使用した非同期処理


型定義を利用して、データフェッチ関数に型を付けます。以下は、fetchを使用した例です。

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }
  return response.json();
}

ここでPromise<Post[]>は、この関数がPost型の配列を返すことを表しています。これにより、呼び出し元での型推論が正確になります。

型定義をReactで活用する


Reactコンポーネントでフェッチしたデータを使用する場合にも、型定義が役立ちます。以下は、Reactで型定義を活用する例です。

import React, { useEffect, useState } from "react";

type Post = {
  id: number;
  title: string;
  body: string;
};

function App() {
  const [posts, setPosts] = useState<Post[] | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => {
        if (!response.ok) {
          throw new Error("Failed to fetch posts");
        }
        return response.json();
      })
      .then((data: Post[]) => {
        setPosts(data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

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

  return (
    <div>
      <h1>Posts:</h1>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

この例では、Post型をuseStateフックと非同期処理の両方で使用しています。これにより、コード全体の型安全性が確保されます。

APIレスポンスが不確定な場合の型定義


APIレスポンスが予測しにくい場合は、Partialunknownを使う方法もあります。

type PartialPost = Partial<Post>; // 部分的な型

こうした柔軟な型付けを活用することで、さまざまなAPIに対応可能です。

次章では、さらに高度な使用例として、Reactクエリとの組み合わせによる型定義について解説します。

Reactクエリとの組み合わせ

Reactクエリ(react-query)は、非同期データフェッチとキャッシュ管理を効率化するための強力なライブラリです。この章では、ReactクエリをTypeScriptと組み合わせて型安全に使用する方法を解説します。

Reactクエリの基本概要


Reactクエリは、非同期データを簡潔かつ効率的に管理できるツールです。以下のような特徴があります。

  • データのキャッシュ機能
  • ローディング状態やエラーハンドリングの簡易化
  • 自動リフェッチ(データの更新)機能

これらの機能をTypeScriptと組み合わせることで、さらに安全かつ堅牢なデータ管理が可能になります。

Reactクエリのセットアップ


Reactクエリを使用するには、まずインストールを行います。

npm install @tanstack/react-query

次に、Reactアプリケーションにクエリクライアントを設定します。

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}

非同期データフェッチと型付け


ReactクエリのuseQueryフックを使用して、非同期データフェッチを型安全に実行します。

import { useQuery } from "@tanstack/react-query";

type Post = {
  id: number;
  title: string;
  body: string;
};

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }
  return response.json();
}

function MyComponent() {
  const { data, isLoading, error } = useQuery<Post[]>(["posts"], fetchPosts);

  if (isLoading) return <p>Loading...</p>;
  if (error instanceof Error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Posts:</h1>
      <ul>
        {data?.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

ポイント

  • useQueryに渡す型引数(<Post[]>)により、フェッチ結果の型を明確に定義しています。
  • fetchPosts関数も型安全に実装されています。

キャッシュ管理と再利用性


Reactクエリでは、クエリキー(["posts"]など)を利用してキャッシュを管理します。これにより、同じデータをリフェッチする際にキャッシュを再利用でき、パフォーマンスを向上させます。

さらに、カスタムフックを作成して型安全なデータフェッチロジックを再利用することも可能です。

import { useQuery } from "@tanstack/react-query";

function usePosts() {
  return useQuery<Post[]>(["posts"], fetchPosts);
}

function MyComponent() {
  const { data, isLoading, error } = usePosts();

  if (isLoading) return <p>Loading...</p>;
  if (error instanceof Error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Posts:</h1>
      <ul>
        {data?.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

TypeScriptの利点を最大限に活用


Reactクエリでは、型を活用して以下のようなメリットを得られます。

  • APIレスポンスが変更された場合でも、型チェックによりすぐに気付ける。
  • IDEの補完機能が強化され、データ操作が直感的になる。
  • チームでの開発において、コードの理解や保守性が向上する。

次章では、AxiosやFetch APIを使用する場合の型付けについて解説します。

AxiosやFetch APIの利用時の型付け

非同期データフェッチを行う際に、多くの開発者がAxiosFetch APIを使用します。これらのツールをTypeScriptで型安全に利用する方法について解説します。

Axiosの型付け

Axiosは、HTTPリクエストを簡単に扱えるライブラリです。TypeScriptと組み合わせることで、リクエストやレスポンスに型を付けることができます。

基本的な型付けの例

以下は、Axiosを用いて型安全にデータを取得する例です。

import axios from "axios";

type Post = {
  id: number;
  title: string;
  body: string;
};

async function fetchPosts(): Promise<Post[]> {
  const response = await axios.get<Post[]>("https://jsonplaceholder.typicode.com/posts");
  return response.data;
}
  • axios.get<Post[]>の部分で、レスポンスデータがPost[]型であることを指定しています。
  • レスポンスのデータはresponse.dataとして取得できます。

エラーハンドリング

Axiosのエラーハンドリングも型安全に行えます。

async function fetchPostsWithErrorHandling(): Promise<Post[]> {
  try {
    const response = await axios.get<Post[]>("https://jsonplaceholder.typicode.com/posts");
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(error.response?.data || "An error occurred");
    }
    throw error;
  }
}

型を使用したリクエストの送信

リクエストボディに型を付けることも可能です。

type NewPost = {
  title: string;
  body: string;
};

async function createPost(newPost: NewPost): Promise<Post> {
  const response = await axios.post<Post>("https://jsonplaceholder.typicode.com/posts", newPost);
  return response.data;
}

Fetch APIの型付け

Fetch APIは、ブラウザに組み込まれている軽量なHTTPリクエストライブラリです。以下に、Fetch APIで型安全にデータを取得する方法を示します。

基本的な型付けの例

type Post = {
  id: number;
  title: string;
  body: string;
};

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }
  return response.json();
}

ジェネリック型を利用した汎用的なデータ取得関数

Fetch APIをジェネリック型と組み合わせることで、どのようなデータ型にも対応できる汎用的な関数を作成できます。

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("Failed to fetch data");
  }
  return response.json();
}

// 使用例
const posts = await fetchData<Post[]>("https://jsonplaceholder.typicode.com/posts");

エラーハンドリング

Fetch APIでも、レスポンスの型を確認しながらエラーを処理できます。

async function fetchPostsWithErrorHandling(): Promise<Post[]> {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    if (!response.ok) {
      throw new Error("Failed to fetch posts");
    }
    return await response.json();
  } catch (error) {
    console.error("Error fetching posts:", error);
    throw error;
  }
}

AxiosとFetch APIの使い分け

特徴AxiosFetch API
設定の柔軟性高い。共通ヘッダーやインターセプターを簡単に設定可能。限定的。必要に応じてカスタマイズが必要。
レスポンスの処理自動的にJSONデータを解析して提供。明示的に.json()を呼び出す必要がある。
エラーハンドリング組み込みのエラーハンドリング機能あり。明示的にresponse.okを確認する必要がある。

状況に応じてこれらを使い分けることで、型安全かつ効率的なデータフェッチが可能になります。次章では、型安全なエラーハンドリングの具体的な実装方法を解説します。

型安全なエラーハンドリングの実装

非同期データフェッチにおけるエラーハンドリングは、アプリケーションの信頼性を高める重要な要素です。TypeScriptを使用することで、エラーハンドリングを型安全に行い、予期しない動作を防ぐことができます。この章では、Reactアプリケーションでの型安全なエラーハンドリングの具体例を紹介します。

基本的なエラーハンドリング


非同期関数でエラーが発生する可能性がある場合、try-catchブロックを使用してエラーハンドリングを行います。TypeScriptでは、エラーオブジェクトの型を明確に定義することで、エラーハンドリングをより安全に実装できます。

type ApiError = {
  message: string;
  status: number;
};

async function fetchPosts(): Promise<Post[]> {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    if (!response.ok) {
      throw {
        message: "Failed to fetch posts",
        status: response.status,
      } as ApiError;
    }
    return await response.json();
  } catch (error) {
    if (isApiError(error)) {
      console.error(`API Error: ${error.message} (Status: ${error.status})`);
    } else {
      console.error("Unknown error occurred");
    }
    throw error;
  }
}

function isApiError(error: any): error is ApiError {
  return error && typeof error.message === "string" && typeof error.status === "number";
}

この例では、isApiErrorという型ガード関数を使用してエラーの型を確認し、適切な処理を行っています。

Reactコンポーネントでのエラーハンドリング


Reactコンポーネントでエラーを表示する場合、状態管理を使用してエラー情報を格納します。

function PostsComponent() {
  const [posts, setPosts] = useState<Post[] | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<ApiError | null>(null);

  useEffect(() => {
    fetchPosts()
      .then((data) => {
        setPosts(data);
        setLoading(false);
      })
      .catch((error) => {
        setError(error as ApiError);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message} (Status: {error.status})</p>;

  return (
    <ul>
      {posts?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

カスタムエラー型を用いたエラーハンドリング


エラーオブジェクトにカスタム型を使用すると、より詳細な情報を格納できます。

type CustomError = {
  type: "network" | "api" | "unknown";
  message: string;
};

async function fetchWithCustomError(): Promise<Post[]> {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    if (!response.ok) {
      throw { type: "api", message: "API responded with an error" } as CustomError;
    }
    return await response.json();
  } catch (error) {
    if (error instanceof Error) {
      throw { type: "network", message: error.message } as CustomError;
    }
    throw { type: "unknown", message: "An unknown error occurred" } as CustomError;
  }
}

このようにカスタム型を定義することで、エラーの種類ごとに詳細な情報を提供し、より柔軟なエラーハンドリングが可能になります。

Reactクエリとの統合


Reactクエリを使用している場合、エラーハンドリングがさらに簡単になります。Reactクエリは、エラーを自動的に状態として管理し、コンポーネントに渡します。

import { useQuery } from "@tanstack/react-query";

function PostsWithReactQuery() {
  const { data, isLoading, error } = useQuery<Post[], ApiError>(
    ["posts"],
    async () => {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts");
      if (!response.ok) {
        throw { message: "Failed to fetch posts", status: response.status } as ApiError;
      }
      return response.json();
    }
  );

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message} (Status: {error.status})</p>;

  return (
    <ul>
      {data?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

エラーハンドリングのベストプラクティス

  • 型ガード関数を活用: エラーオブジェクトの型を確認して安全に処理する。
  • 適切なエラーメッセージ: ユーザーにわかりやすいメッセージを表示する。
  • 統一的なエラーハンドリング: カスタム型やユーティリティ関数を活用して一貫性を保つ。

次章では、実践例として、型安全な非同期データフェッチの具体的なコードを解説します。

実践例:ユーザーリストのフェッチと表示

ここでは、TypeScriptを使用して型安全にユーザーリストを非同期フェッチし、Reactコンポーネントで表示する実践例を紹介します。この例を通じて、データフェッチの流れと型付けのポイントを確認できます。

APIレスポンスの型定義


まず、フェッチするAPIのデータ構造を調査し、それに基づいて型を定義します。以下は、仮想APIから返されるユーザーのレスポンス例です。

APIレスポンス例:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
  },
  {
    "id": 2,
    "name": "Jane Smith",
    "email": "jane.smith@example.com"
  }
]

これをTypeScriptで型定義します。

type User = {
  id: number;
  name: string;
  email: string;
};

データフェッチ関数


次に、非同期関数を作成し、型を使用してAPIレスポンスの構造を保証します。

async function fetchUsers(): Promise<User[]> {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  if (!response.ok) {
    throw new Error("Failed to fetch users");
  }
  return response.json();
}

Reactコンポーネントでの表示


ユーザーリストを取得し、Reactコンポーネントで表示します。

import React, { useEffect, useState } from "react";

function UserList() {
  const [users, setUsers] = useState<User[] | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUsers()
      .then((data) => {
        setUsers(data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>
            <strong>{user.name}</strong> ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

コードの解説

  1. fetchUsers関数: APIからユーザーデータを取得し、レスポンスデータがUser[]型であることを保証します。
  2. useStateフック: ユーザー情報、ローディング状態、エラーメッセージを管理します。
  3. useEffectフック: コンポーネントがマウントされたときにデータを取得します。
  4. UIの状態管理: ローディング、エラー、データ取得後の状態を適切に切り替えて表示します。

最適化:カスタムフックの活用


データフェッチロジックをカスタムフックとして分離すると、再利用性が高まりコードが簡潔になります。

function useUsers() {
  const [users, setUsers] = useState<User[] | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUsers()
      .then((data) => {
        setUsers(data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  return { users, loading, error };
}

function UserListWithHook() {
  const { users, loading, error } = useUsers();

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>
            <strong>{user.name}</strong> ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}

結果


この実践例により、型安全なデータフェッチと、ユーザーリストの効率的な表示が実現できます。さらにカスタムフックを活用することで、再利用性の高いコードを構築できます。

次章では、カスタムフックをさらに拡張して、複数のAPIに対応可能な再利用可能な非同期処理を解説します。

応用:カスタムフックを使った再利用可能な非同期処理

非同期データフェッチは多くのReactプロジェクトで頻繁に必要とされます。カスタムフックを活用することで、非同期処理のロジックを簡潔かつ再利用可能にすることができます。この章では、型安全な汎用カスタムフックを作成する方法を紹介します。

汎用的な非同期処理カスタムフックの設計

カスタムフックuseFetchを作成し、任意のAPIエンドポイントとデータ型に対応するように設計します。ジェネリック型を使用して、データの型を動的に指定できるようにします。

import { useState, useEffect } from "react";

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

function useFetch<T>(url: string): FetchState<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Error: ${response.status}`);
        }
        const result: T = await response.json();
        setData(result);
      } catch (err) {
        setError((err as Error).message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

カスタムフックの使用例

このuseFetchフックを使用して、複数のエンドポイントからデータを取得する例を示します。

type User = {
  id: number;
  name: string;
  email: string;
};

type Post = {
  id: number;
  title: string;
  body: string;
};

function UsersComponent() {
  const { data: users, loading, error } = useFetch<User[]>("https://jsonplaceholder.typicode.com/users");

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users?.map((user) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

function PostsComponent() {
  const { data: posts, loading, error } = useFetch<Post[]>("https://jsonplaceholder.typicode.com/posts");

  if (loading) return <p>Loading posts...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>
            <strong>{post.title}</strong>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

汎用カスタムフックの利点

  1. 再利用性の向上: useFetchフックはジェネリック型を使用しているため、どのようなデータ型にも対応可能です。
  2. コードの簡素化: データフェッチに関する重複コードを削減し、コンポーネントのロジックを簡潔にします。
  3. 型安全性の確保: TypeScriptによる型付けにより、フェッチデータの構造が保証されます。
  4. ロジックの集中管理: データフェッチのロジックを1か所にまとめることで、メンテナンスが容易になります。

カスタムフックの拡張

追加機能を加えることで、さらに柔軟なカスタムフックにすることも可能です。

機能追加例:

  1. パラメータのサポート: 動的なクエリパラメータを受け取る機能を追加。
  2. ポーリング: 一定間隔でデータを更新する機能を追加。
  3. キャンセル処理: コンポーネントのアンマウント時に未完了のリクエストをキャンセル。
function useFetchWithParams<T>(url: string, params: Record<string, string>) {
  const query = new URLSearchParams(params).toString();
  return useFetch<T>(`${url}?${query}`);
}

このように、useFetchはアプリケーションのさまざまな要件に合わせて柔軟に拡張できます。

次章では、この記事全体の内容をまとめ、ReactとTypeScriptを使用した非同期データフェッチの効果的な方法を再確認します。

まとめ

本記事では、ReactとTypeScriptを用いた非同期データフェッチの型安全な実装方法について詳しく解説しました。非同期データフェッチの基本から、TypeScriptによる型定義、ReactクエリやAxios、Fetch APIの活用、そして再利用可能なカスタムフックの設計まで、具体的な手法を段階的に示しました。

型安全性を確保することで、予期しないエラーを防ぎ、開発効率を大幅に向上させることができます。特に、カスタムフックを活用することで、複雑なアプリケーションにおいてもコードの再利用性を高め、保守性を向上させることが可能です。これらの知識を応用して、より効率的で信頼性の高いReactアプリケーションを構築してください。

コメント

コメントする

目次
  1. 非同期データフェッチの基本概要
    1. Reactでの非同期処理の流れ
    2. 基本的なコード例
  2. TypeScriptを使用する利点
    1. 1. 型による明確なデータ構造の定義
    2. 2. 型推論による開発効率の向上
    3. 3. 型安全なエラーハンドリング
    4. 4. 開発時の補完とドキュメンテーションの強化
    5. 5. チーム開発における信頼性の向上
  3. 型定義の基本と実例
    1. 基本的な型定義
    2. 型定義を使用した非同期処理
    3. 型定義をReactで活用する
    4. APIレスポンスが不確定な場合の型定義
  4. Reactクエリとの組み合わせ
    1. Reactクエリの基本概要
    2. Reactクエリのセットアップ
    3. 非同期データフェッチと型付け
    4. キャッシュ管理と再利用性
    5. TypeScriptの利点を最大限に活用
  5. AxiosやFetch APIの利用時の型付け
    1. Axiosの型付け
    2. Fetch APIの型付け
    3. AxiosとFetch APIの使い分け
  6. 型安全なエラーハンドリングの実装
    1. 基本的なエラーハンドリング
    2. Reactコンポーネントでのエラーハンドリング
    3. カスタムエラー型を用いたエラーハンドリング
    4. Reactクエリとの統合
    5. エラーハンドリングのベストプラクティス
  7. 実践例:ユーザーリストのフェッチと表示
    1. APIレスポンスの型定義
    2. データフェッチ関数
    3. Reactコンポーネントでの表示
    4. コードの解説
    5. 最適化:カスタムフックの活用
    6. 結果
  8. 応用:カスタムフックを使った再利用可能な非同期処理
    1. 汎用的な非同期処理カスタムフックの設計
    2. カスタムフックの使用例
    3. 汎用カスタムフックの利点
    4. カスタムフックの拡張
  9. まとめ