Reactでの非同期処理をPromiseやasync/awaitで効率的に管理する方法

非同期処理は、Reactアプリケーション開発において避けて通れない重要なテーマです。特に、ユーザーインターフェースをスムーズに動作させるために、データの取得、ファイルの読み書き、またはタイマー操作などの非同期タスクを適切に管理する必要があります。その際、Promiseやasync/awaitといったJavaScriptの非同期処理メカニズムを理解し、効果的に活用することが、コードの可読性と保守性を向上させる鍵となります。本記事では、Reactでの非同期処理の基本概念から実践的な応用例までを網羅し、エラー処理や状態管理ライブラリとの統合方法にも触れながら、開発者が直面する課題の解決を支援します。

目次

Reactで非同期処理が必要な場面とは


Reactアプリケーションでは、非同期処理を必要とする場面が数多く存在します。これらの非同期タスクを正しく管理することで、ユーザー体験を向上させることができます。

データの取得と表示


Reactで最も一般的な非同期処理の場面は、外部APIからデータを取得し、それをコンポーネントに表示するケースです。たとえば、REST APIやGraphQLからユーザーデータや商品リストを取得し、画面に反映する場合です。

ユーザーアクションへの応答


ボタンをクリックしたときにバックエンドと通信してデータを送信する、フォームの入力内容を検証するなど、ユーザーアクションに基づいて非同期タスクを処理する必要があります。

タイマーやアニメーション


非同期処理は、タイマーやアニメーションなどの時間依存の操作にも必要です。これには、特定の時間後に状態を更新したり、一定間隔でデータをポーリングする場合が含まれます。

ファイル操作


ファイルのアップロードやダウンロードも非同期処理が必要な典型例です。大きなファイルを処理する場合、非同期でタスクを実行しないとアプリケーション全体がフリーズしてしまう可能性があります。

バックエンドとのリアルタイム通信


WebSocketやServer-Sent Eventsを用いたリアルタイム通信は、非同期処理の別の重要な領域です。チャットアプリや通知機能など、リアルタイムでデータが更新される場合に活用されます。

Reactアプリでの非同期処理の重要性


これらの場面で非同期処理を正しく行わないと、アプリケーションのパフォーマンスや安定性が大きく損なわれます。また、ユーザー体験を向上させるためには、スムーズなローディングやエラーハンドリングも不可欠です。Reactでは、これらのタスクを効率的に管理するためにPromiseやasync/await、または状態管理ライブラリを使用することが一般的です。

Promiseの基本とReactでの使用例

Promiseの基本概念


Promiseは、非同期操作の結果を表現するオブジェクトです。「非同期処理が成功した」「失敗した」などの状態を管理し、その結果を操作するためのメソッド(thencatchなど)を提供します。Promiseは3つの状態を持ちます:

  1. Pending(保留中): 初期状態。非同期処理がまだ完了していない状態です。
  2. Fulfilled(成功): 非同期処理が成功し、結果が得られた状態です。
  3. Rejected(失敗): 非同期処理が失敗し、エラーが発生した状態です。

Promiseの基本的な書き方


以下は、Promiseを使用した非同期処理の基本例です:

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true; // 成功した場合と失敗した場合を分ける条件
      if (success) {
        resolve("データ取得に成功しました!");
      } else {
        reject("エラーが発生しました。");
      }
    }, 2000);
  });
};

fetchData()
  .then((result) => {
    console.log(result); // データ取得に成功した場合の処理
  })
  .catch((error) => {
    console.error(error); // エラー発生時の処理
  });

ReactでのPromiseの使用例

PromiseはReactのuseEffectフック内で頻繁に利用されます。たとえば、APIからデータを取得する際に使用されます。

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

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

  useEffect(() => {
    const fetchData = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true;
          if (success) {
            resolve("サーバーからのデータ");
          } else {
            reject("エラーが発生しました。");
          }
        }, 2000);
      });
    };

    fetchData()
      .then((result) => {
        setData(result);
      })
      .catch((err) => {
        setError(err);
      });
  }, []);

  return (
    <div>
      {error && <p>{error}</p>}
      {data ? <p>{data}</p> : <p>データを取得しています...</p>}
    </div>
  );
}

export default App;

PromiseのReactでの活用のメリット

  • 明確な非同期処理の流れ: Promiseのthencatchを用いることで、非同期処理の成功時や失敗時の流れを簡潔に記述できます。
  • コードの可読性: コールバック地獄を回避し、非同期コードを読みやすく整理できます。

Promiseを活用することで、非同期処理をシンプルかつ明確に管理でき、Reactアプリケーションの可読性と保守性を高めることができます。

async/awaitの基本とReactでの使用例

async/awaitの基本概念


async/awaitは、Promiseを扱うための構文で、非同期処理を同期処理のように記述できます。これにより、非同期コードの可読性が向上し、直感的に理解しやすくなります。

  • async関数: 関数の前にasyncをつけることで、その関数がPromiseを返すようになります。
  • await: awaitを使うことで、Promiseが解決(成功または失敗)されるまで待機し、その結果を変数に代入できます。

基本的な使用例

以下は、非同期処理をasync/awaitで書き直した例です:

const fetchData = async () => {
  try {
    const result = await new Promise((resolve, reject) => {
      setTimeout(() => {
        const success = true;
        if (success) {
          resolve("データ取得に成功しました!");
        } else {
          reject("エラーが発生しました。");
        }
      }, 2000);
    });
    console.log(result); // データ取得に成功した場合の処理
  } catch (error) {
    console.error(error); // エラー発生時の処理
  }
};

fetchData();

Reactでのasync/awaitの使用例

Reactコンポーネント内でasync/awaitを利用する場合、特にuseEffectフックと組み合わせることが一般的です。ただし、useEffect自体は同期的な関数しか受け付けないため、非同期処理を呼び出す関数を内部で作成する必要があります。

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

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await new Promise((resolve, reject) => {
          setTimeout(() => {
            const success = true;
            if (success) {
              resolve("サーバーからのデータ");
            } else {
              reject("エラーが発生しました。");
            }
          }, 2000);
        });
        setData(response); // 成功時にデータを状態に保存
      } catch (err) {
        setError(err); // エラー発生時にエラーメッセージを保存
      }
    };

    fetchData();
  }, []);

  return (
    <div>
      {error && <p>{error}</p>}
      {data ? <p>{data}</p> : <p>データを取得しています...</p>}
    </div>
  );
}

export default App;

async/awaitを使用するメリット

  1. コードの可読性: 処理が順次進むように見えるため、コールバックのネストやPromiseチェーンに比べて読みやすい。
  2. エラーハンドリングが簡単: try-catchブロックを使うことで、非同期処理中のエラーを一元的に管理できる。
  3. デバッグが容易: 非同期コードでも同期コードのようにステップ実行が可能で、エラー箇所を特定しやすい。

Reactでの適用時の注意点

  • useEffect内で直接asyncを使わない: useEffectのコールバック関数にasyncを付けると、期待しない挙動が発生する可能性があります。そのため、非同期処理を内部関数で分離することが推奨されます。
  • パフォーマンスへの配慮: 非同期処理が完了する前にコンポーネントがアンマウントされる場合、状態更新の処理がエラーを引き起こす可能性があるため、クリーンアップ関数を適切に利用することが重要です。

async/awaitを用いることで、非同期処理が直感的かつ簡潔に記述でき、Reactアプリケーションのコード品質を向上させることが可能です。

useEffectと非同期処理の組み合わせ方

useEffectで非同期処理を扱う基本ルール


ReactのuseEffectフックは、コンポーネントがマウントされた際や、依存関係の値が更新された際に特定の処理を実行するために使用されます。非同期処理をuseEffectで扱う際には、以下の基本ルールを理解しておくことが重要です:

  1. useEffect内で直接asyncを使わない: useEffectのコールバック関数は同期的である必要があるため、直接asyncを付けるとエラーや予期しない挙動を招く可能性があります。代わりに内部で非同期関数を宣言して呼び出します。
  2. 依存関係配列を正しく設定する: 無駄な再レンダリングや非同期処理の再実行を防ぐため、適切に依存関係を指定します。

基本的なuseEffectと非同期処理の組み合わせ例


以下は、useEffectでAPIデータを取得する例です:

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

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

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true); // ローディング開始
      try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
          throw new Error("データの取得に失敗しました");
        }
        const result = await response.json();
        setData(result); // データを状態に保存
      } catch (err) {
        setError(err.message); // エラーメッセージを保存
      } finally {
        setLoading(false); // ローディング終了
      }
    };

    fetchData();
  }, []); // 空の依存配列:マウント時のみ実行

  return (
    <div>
      {loading && <p>データを取得しています...</p>}
      {error && <p>エラー: {error}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

export default App;

クリーンアップを考慮した非同期処理


コンポーネントがアンマウントされる際に非同期処理を正しくキャンセルしないと、状態更新エラーが発生する可能性があります。この問題を防ぐために、useEffect内でフラグを使って安全に状態を更新します。

useEffect(() => {
  let isMounted = true; // マウント状態を追跡するフラグ

  const fetchData = async () => {
    try {
      const response = await fetch("https://api.example.com/data");
      const result = await response.json();
      if (isMounted) {
        setData(result); // マウント中の場合のみ状態を更新
      }
    } catch (err) {
      if (isMounted) {
        setError(err.message);
      }
    }
  };

  fetchData();

  return () => {
    isMounted = false; // アンマウント時にフラグを更新
  };
}, []);

依存関係配列を適切に設定する


useEffectの依存関係配列を正しく指定することで、非同期処理が予期せず繰り返し実行される問題を防ぎます。以下のように依存関係を明示します:

useEffect(() => {
  const fetchFilteredData = async () => {
    try {
      const response = await fetch(`https://api.example.com/data?filter=${filter}`);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    }
  };

  fetchFilteredData();
}, [filter]); // filterが変更されたときのみ再実行

useEffectと非同期処理を組み合わせるメリット

  1. 状態管理が簡単: 非同期処理の実行と結果の管理をReactの状態に統合できます。
  2. 再レンダリングの抑制: 依存関係配列を使うことで、無駄な処理を防ぎ効率的な非同期処理が可能です。
  3. クリーンアップで安全性向上: フラグやクリーンアップ関数を使うことで、不要な状態更新を防ぎ、アプリケーションの安定性を確保できます。

非同期処理を適切に組み合わせたuseEffectを使うことで、Reactアプリケーションのデータ取得やユーザー体験をよりスムーズに管理できます。

非同期エラーのハンドリング方法

非同期処理におけるエラーの重要性


Reactアプリケーションでは、非同期処理中にエラーが発生することは避けられません。ネットワークの不調、APIのレスポンスエラー、データフォーマットの不一致など、多くの原因が考えられます。エラーを適切にハンドリングすることで、ユーザーに適切なフィードバックを提供し、アプリの信頼性を向上させることができます。

Promiseを使用したエラーハンドリング


Promiseを使用する場合、catchメソッドを活用してエラーをハンドリングします。

const fetchData = () => {
  return fetch("https://api.example.com/data")
    .then((response) => {
      if (!response.ok) {
        throw new Error("データ取得に失敗しました");
      }
      return response.json();
    })
    .catch((error) => {
      console.error("エラーが発生しました:", error.message);
    });
};

fetchData();

ポイント:

  • 必要に応じてエラー内容をユーザーに表示します。
  • エラーログを記録して問題解決に役立てます。

async/awaitを使用したエラーハンドリング


async/awaitでは、try-catchブロックを使ってエラーをハンドリングします。

const fetchData = async () => {
  try {
    const response = await fetch("https://api.example.com/data");
    if (!response.ok) {
      throw new Error("データ取得に失敗しました");
    }
    const data = await response.json();
    console.log("取得したデータ:", data);
  } catch (error) {
    console.error("エラーが発生しました:", error.message);
  }
};

fetchData();

ポイント:

  • ネットワークエラーやレスポンスエラーを1つのcatchでまとめて扱えます。
  • 非同期コードが同期コードに近い形で記述できるため、可読性が向上します。

Reactでのエラーハンドリング実例

以下は、非同期処理を含むReactコンポーネントでのエラーハンドリングの例です:

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

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

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
          throw new Error("データ取得に失敗しました");
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  return (
    <div>
      {loading && <p>データを取得しています...</p>}
      {error && <p>エラー: {error}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

export default App;

ポイント:

  • ローディング状態(loading)を管理して、エラー発生時も適切にユーザーインターフェースを表示します。
  • エラーメッセージ(error)を分かりやすくユーザーに伝えます。

非同期エラーをカスタムフックで統一管理


エラーハンドリングのロジックを再利用可能にするため、カスタムフックを作成するのも有効です。

import { useState, useEffect } from "react";

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("データ取得に失敗しました");
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, error, loading };
};

export default useFetch;

利用例:

import React from "react";
import useFetch from "./useFetch";

function App() {
  const { data, error, loading } = useFetch("https://api.example.com/data");

  return (
    <div>
      {loading && <p>データを取得しています...</p>}
      {error && <p>エラー: {error}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

export default App;

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

  1. ユーザーに明確なメッセージを表示: エラー発生時にユーザーが次に何をすべきか分かるようにします(例: 再試行ボタンの表示)。
  2. エラーログを記録: サーバーやローカルストレージにエラーを記録し、デバッグや分析に役立てます。
  3. 冗長なエラーハンドリングを避ける: カスタムフックやユーティリティ関数を使用して再利用可能なロジックを構築します。

エラーを適切にハンドリングすることで、ユーザー体験の向上とアプリケーションの信頼性向上を実現できます。

状態管理ライブラリと非同期処理の統合

非同期処理と状態管理の重要性


Reactアプリケーションでは、非同期処理によって取得したデータを効率的に管理し、コンポーネント間で共有する必要があります。単純なアプリではuseStateuseEffectで十分ですが、アプリが複雑になると、状態管理ライブラリを活用することで効率的にデータを管理できます。ReduxやReact Queryといったライブラリは、非同期処理の統合を簡単にし、エラーハンドリングやキャッシングをサポートします。

Redux Toolkitで非同期処理を管理する

Redux Toolkitを使うと、非同期処理を含む状態管理を簡潔に記述できます。特にcreateAsyncThunkを使うことで、非同期処理を簡単にReduxの状態に統合できます。

非同期処理の例:

  1. Reduxストアを作成し、非同期のスライスを定義します。
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';

// 非同期アクションを定義
export const fetchData = createAsyncThunk('data/fetchData', async () => {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('データ取得に失敗しました');
  }
  return await response.json();
});

// スライスを作成
const dataSlice = createSlice({
  name: 'data',
  initialState: { data: null, status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchData.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchData.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.data = action.payload;
      })
      .addCase(fetchData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

// ストアを作成
const store = configureStore({
  reducer: {
    data: dataSlice.reducer,
  },
});

export default store;
  1. Reactコンポーネントで状態を使用します。
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchData } from './store';

function App() {
  const dispatch = useDispatch();
  const { data, status, error } = useSelector((state) => state.data);

  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  return (
    <div>
      {status === 'loading' && <p>データを取得しています...</p>}
      {status === 'failed' && <p>エラー: {error}</p>}
      {status === 'succeeded' && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

export default App;

ポイント:

  • Redux ToolkitのcreateAsyncThunkを使うと、非同期処理の状態(pendingfulfilledrejected)を自動的に追跡できます。
  • 状態やエラーを一元管理でき、複雑なアプリケーションでも効率的に非同期処理を扱えます。

React Queryで非同期処理を管理する

React Queryは、サーバー状態を効率的に管理するためのライブラリで、キャッシングやデータフェッチの再試行などの非同期処理を簡単に扱うことができます。

非同期処理の例:

  1. React Queryをセットアップします。
import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

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

export default AppWrapper;
  1. データフェッチ用のフックを利用します。
import React from 'react';
import { useQuery } from 'react-query';

function App() {
  const { data, error, isLoading, isError } = useQuery('fetchData', async () => {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('データ取得に失敗しました');
    }
    return await response.json();
  });

  return (
    <div>
      {isLoading && <p>データを取得しています...</p>}
      {isError && <p>エラー: {error.message}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

export default App;

ポイント:

  • React Queryは、データのキャッシングと更新を自動化し、データフェッチを効率化します。
  • 再試行、ローディング状態、エラー処理が組み込みで提供されており、開発者の負担を軽減します。

Redux ToolkitとReact Queryの選択基準

  • Redux Toolkit: アプリ全体の状態(フォームデータやUIステートなど)を管理しつつ、非同期処理も扱いたい場合に最適です。
  • React Query: 非同期データの取得やキャッシングに特化しており、サーバー状態のみを扱う場合に最適です。

非同期処理と状態管理ライブラリの統合のメリット

  1. 一元管理: データや状態の管理が統一され、コードの可読性と保守性が向上します。
  2. エラーハンドリングの標準化: 状態管理ライブラリに組み込まれたエラーハンドリング機能を活用できます。
  3. 効率的なキャッシング: React Queryのようなライブラリではデータのキャッシングが簡単に実現できます。

適切なライブラリを選択することで、Reactアプリケーションの非同期処理が効率化され、開発速度と品質が大幅に向上します。

応用例:APIデータの取得と状態管理

APIデータ取得の流れ


Reactアプリケーションでは、APIからデータを取得し、そのデータを状態管理ライブラリで管理することがよくあります。このセクションでは、以下のシナリオを例にとり、非同期処理をReactに統合する方法を解説します:

  • ユーザー一覧をAPIから取得して表示
  • ユーザーをクリックすると詳細情報を取得

APIデータ取得の基本的なコード例


以下は、ユーザー一覧を取得して表示する基本的な例です。この例ではReact Queryを使用しています。

コード例:

import React from 'react';
import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  if (!response.ok) {
    throw new Error('ユーザー一覧の取得に失敗しました');
  }
  return response.json();
};

function UserList() {
  const { data: users, isLoading, isError, error } = useQuery('users', fetchUsers);

  if (isLoading) return <p>読み込み中...</p>;
  if (isError) return <p>エラー: {error.message}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

ポイント:

  • React QueryのuseQueryフックを使い、APIデータを非同期に取得しています。
  • ローディング、エラー、成功の状態を簡潔に管理できます。

詳細データの取得

次に、ユーザーをクリックすると、そのユーザーの詳細データを取得して表示する機能を追加します。

コード例:

import React, { useState } from 'react';
import { useQuery } from 'react-query';

const fetchUserDetails = async (userId) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  if (!response.ok) {
    throw new Error('ユーザー詳細の取得に失敗しました');
  }
  return response.json();
};

function UserDetails({ userId }) {
  const { data: user, isLoading, isError, error } = useQuery(
    ['userDetails', userId],
    () => fetchUserDetails(userId),
    { enabled: !!userId } // userIdがある場合のみ実行
  );

  if (!userId) return <p>ユーザーを選択してください</p>;
  if (isLoading) return <p>読み込み中...</p>;
  if (isError) return <p>エラー: {error.message}</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>メール: {user.email}</p>
      <p>電話番号: {user.phone}</p>
    </div>
  );
}

function App() {
  const [selectedUserId, setSelectedUserId] = useState(null);

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <UserList onSelectUser={setSelectedUserId} />
      <h1>ユーザー詳細</h1>
      <UserDetails userId={selectedUserId} />
    </div>
  );
}

function UserList({ onSelectUser }) {
  const { data: users, isLoading, isError, error } = useQuery('users', fetchUsers);

  if (isLoading) return <p>読み込み中...</p>;
  if (isError) return <p>エラー: {error.message}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onClick={() => onSelectUser(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

export default App;

ポイント:

  • ユーザーIDをクリックすると、そのIDを使って詳細データを取得します。
  • useQueryenabledオプションを使い、IDが選択されたときのみデータ取得を実行しています。

状態管理ライブラリでのデータ共有


アプリがさらに複雑になる場合、Redux ToolkitやContext APIを使ってデータをアプリ全体で共有する方法が適しています。

Redux Toolkitでの例


Redux Toolkitを使えば、取得したユーザー一覧や詳細データをストアに保存し、複数のコンポーネントで共有できます。

// 非同期スライスを定義
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  if (!response.ok) {
    throw new Error('ユーザー一覧の取得に失敗しました');
  }
  return response.json();
});

const userSlice = createSlice({
  name: 'users',
  initialState: { list: [], status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.list = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export const selectAllUsers = (state) => state.users.list;
export const userReducer = userSlice.reducer;

このようにして、非同期処理で取得したデータをストアに保存し、必要な箇所で参照できるようにします。

応用例のメリット

  1. データの再利用: 状態管理ライブラリを活用することで、同じデータを複数のコンポーネントで効率的に共有できます。
  2. パフォーマンス向上: React Queryなどを使うことでキャッシングやデータ再取得の最適化が可能です。
  3. スケーラブルな設計: 非同期処理と状態管理を統合することで、大規模アプリケーションでもメンテナンスしやすい構造を実現できます。

このような応用例を実践することで、Reactアプリケーションの機能を効率的に拡張できます。

演習問題:非同期処理の実装練習

課題1: APIデータの取得と表示


以下の仕様を満たすReactアプリケーションを作成してください:

  1. 目的: 「https://jsonplaceholder.typicode.com/posts」から投稿データを取得し、一覧表示する。
  2. 要件:
  • 非同期処理にはasync/awaitを使用する。
  • エラーが発生した場合はエラーメッセージを表示する。
  • ローディング中は「データを取得しています…」というメッセージを表示する。
  1. 完成イメージ:
  • 投稿一覧が画面に表示される(タイトルとボディ)。
  • エラーやローディング状態に応じてUIが変わる。

ヒント:

  • 状態管理にはuseStateを使用してください。
  • 非同期処理はuseEffect内で実行してください。

解答例

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

function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        if (!response.ok) {
          throw new Error('投稿データの取得に失敗しました');
        }
        const data = await response.json();
        setPosts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []);

  if (loading) return <p>データを取得しています...</p>;
  if (error) return <p>エラー: {error}</p>;

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

export default PostList;

課題2: 詳細データの取得


以下の仕様を満たすReactアプリケーションを作成してください:

  1. 目的: 投稿一覧をクリックすると、詳細情報を取得して表示する。
  2. 要件:
  • 一覧をクリックしたときに、該当の投稿詳細を取得する非同期処理を実行する。
  • 詳細情報には「タイトル」「本文」を含める。
  • 再びクリックして別の投稿を選択できるようにする。
  1. 完成イメージ:
  • 初期表示は投稿一覧のみ。
  • 投稿をクリックすると、その投稿の詳細情報が表示される。

ヒント:

  • 詳細情報の取得には、選択した投稿のIDを使用してください。
  • 状態管理にuseStateを使用してください。

解答例

import React, { useState } from 'react';

function PostDetails({ postId }) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  React.useEffect(() => {
    if (!postId) return;

    const fetchPostDetails = async () => {
      setLoading(true);
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
        if (!response.ok) {
          throw new Error('詳細データの取得に失敗しました');
        }
        const data = await response.json();
        setPost(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchPostDetails();
  }, [postId]);

  if (!postId) return <p>投稿を選択してください</p>;
  if (loading) return <p>詳細データを取得しています...</p>;
  if (error) return <p>エラー: {error}</p>;

  return (
    <div>
      <h3>{post.title}</h3>
      <p>{post.body}</p>
    </div>
  );
}

function App() {
  const [selectedPostId, setSelectedPostId] = useState(null);

  return (
    <div>
      <h1>投稿一覧</h1>
      <PostList onSelectPost={setSelectedPostId} />
      <h1>投稿詳細</h1>
      <PostDetails postId={selectedPostId} />
    </div>
  );
}

function PostList({ onSelectPost }) {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  React.useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        if (!response.ok) {
          throw new Error('投稿データの取得に失敗しました');
        }
        const data = await response.json();
        setPosts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []);

  if (loading) return <p>データを取得しています...</p>;
  if (error) return <p>エラー: {error}</p>;

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

export default App;

演習を通じてのポイント

  • 非同期処理の流れを理解し、useEffectを使ってデータを取得する練習ができます。
  • 複数の状態(一覧と詳細)の管理方法を学べます。
  • エラーハンドリングとローディング状態の表示が重要です。

これらの課題を解くことで、Reactにおける非同期処理の基本と応用を深く理解できます。

まとめ

本記事では、Reactアプリケーションでの非同期処理をPromiseやasync/awaitを使って効率的に管理する方法について解説しました。Promiseの基本概念から始まり、async/awaitを用いた非同期処理の書き方、useEffectとの組み合わせ方、エラーハンドリングの実践、そして状態管理ライブラリとの統合や応用例に至るまで、実用的な技術を幅広く紹介しました。

非同期処理は、Reactアプリケーションを開発するうえで欠かせない技術です。Promiseやasync/awaitを理解し、useEffectや状態管理ライブラリと適切に統合することで、スケーラブルで信頼性の高いアプリケーションを構築できます。これらの知識を活用して、非同期タスクを効率的に処理し、ユーザー体験を向上させるアプリケーション開発を目指しましょう。

コメント

コメントする

目次