Redux ToolkitのcreateAsyncThunkで簡単にAPI呼び出しを実装する方法

Redux Toolkitは、Reactアプリケーションでの状態管理を簡略化し、開発効率を大幅に向上させるための強力なツールです。その中でもcreateAsyncThunkは、API呼び出しのような非同期処理を簡潔に管理するためのメソッドです。この記事では、createAsyncThunkを使って、API呼び出しを効率的に実装する方法を詳しく解説します。これにより、複雑な非同期処理や状態管理をスムーズに実装できるようになります。

目次

Redux ToolkitとcreateAsyncThunkの概要

Redux Toolkitの目的と特徴


Redux Toolkitは、Reduxの公式ツールセットとして提供され、状態管理の簡素化を目的としています。これにより、ボイラープレートコードが削減され、より直感的で効率的な開発が可能です。主要な特徴には以下があります。

  • 簡潔なコードcreateSliceconfigureStoreを使用して、冗長なコードを削減します。
  • 統合されたミドルウェアredux-thunkがデフォルトで統合され、非同期処理が容易になります。

createAsyncThunkの役割


createAsyncThunkは、非同期処理をReduxの状態管理フローに組み込むためのメソッドです。API呼び出しやデータ取得など、非同期操作のライフサイクル(pendingfulfilledrejected)を一元管理できます。

主な利点

  1. 非同期処理の状態管理が簡単になる。
  2. 状態の変化に応じて自動的にアクションが生成される。
  3. エラーハンドリングが統一され、コードが読みやすくなる。

これらの機能により、createAsyncThunkは、複雑な非同期処理の実装を簡略化する重要なツールとなっています。

createAsyncThunkの基本的な構文と使い方

基本構文


createAsyncThunkは、非同期アクションを作成するためのRedux Toolkitの関数です。以下の基本構文を理解することで、簡単に非同期処理を実装できます。

import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchData = createAsyncThunk(
  'data/fetchData', // アクション名
  async (arg, thunkAPI) => {
    const response = await fetch('/api/data'); // API呼び出し
    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    return await response.json(); // 成功時のデータを返す
  }
);

構成要素の解説

  1. アクション名: 'data/fetchData' のように、非同期アクションの一意な識別子を指定します。
  2. 非同期関数: async (arg, thunkAPI) => {} の形式で記述され、fetchaxiosを使ったAPI呼び出しや、データ処理を行います。
  3. 引数 (arg): 呼び出し元から渡されるオプションの引数で、APIエンドポイントやフィルター条件を受け渡すために使います。
  4. thunkAPI: 現在のdispatchgetStateなど、追加のツールを提供します。

使用例


以下は、fetchDataをReactコンポーネントで使う基本例です。

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './dataSlice';

const DataComponent = () => {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.data.items);
  const status = useSelector((state) => state.data.status);

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

  if (status === 'loading') return <p>Loading...</p>;
  if (status === 'failed') return <p>Error fetching data.</p>;

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

export default DataComponent;

動作の流れ

  1. コンポーネントがマウントされるとfetchDatadispatchされます。
  2. createAsyncThunkが非同期処理を開始し、状態をpendingに更新します。
  3. 処理成功でfulfilledアクションが実行され、データが状態に保存されます。
  4. 処理失敗でrejectedアクションが実行され、エラー状態が管理されます。

このように、createAsyncThunkを利用することで、簡潔でメンテナンス性の高い非同期処理が可能になります。

非同期処理の状態管理とライフサイクル

非同期処理の3つのライフサイクル


createAsyncThunkで生成される非同期アクションには、以下の3つのライフサイクルが自動的に管理されます。

  1. pending
    非同期処理が開始された状態。API呼び出しが実行中の間、この状態が設定されます。
  2. fulfilled
    非同期処理が成功し、データが正常に取得された状態。この時点で、サーバーからのレスポンスがReduxの状態に格納されます。
  3. rejected
    非同期処理が失敗した状態。APIエラーやネットワークエラーが発生した場合、この状態が設定されます。

ライフサイクルの状態管理


createAsyncThunkを利用することで、これらの状態が自動的にアクションとして作成され、Reduxストアに反映されます。以下は基本的なリデューサーの例です。

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 非同期アクション
export const fetchData = createAsyncThunk('data/fetchData', async () => {
  const response = await fetch('/api/data');
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  return await response.json();
});

// スライスの定義
const dataSlice = createSlice({
  name: 'data',
  initialState: {
    items: [],
    status: 'idle', // 'idle', 'loading', 'succeeded', 'failed'
    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.items = action.payload;
      })
      .addCase(fetchData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export default dataSlice.reducer;

ステータスとUIの連携


状態の変化に応じて、UIを更新することで、ユーザーに非同期処理の進捗状況を伝えることができます。

if (status === 'loading') return <p>データを読み込んでいます...</p>;
if (status === 'succeeded') return <p>データが正常に取得されました!</p>;
if (status === 'failed') return <p>エラーが発生しました: {error}</p>;

ポイント

  • pendingでローディングスピナーを表示するなど、直感的なUIを提供します。
  • rejectedで適切なエラーメッセージを表示し、ユーザー体験を損なわない工夫をします。

まとめ


createAsyncThunkによる非同期処理のライフサイクル管理は、API呼び出しの状態を直感的に追跡できるため、Reactアプリケーションにおける状態管理が非常に効率的になります。この仕組みを活用することで、アプリケーションの信頼性と使いやすさを向上させることができます。

サンプルコード: ユーザーリストの取得

APIからユーザーリストを取得する例


以下は、createAsyncThunkを使って、APIからユーザーリストを取得するサンプルコードです。この例では、サーバーからユーザー情報を取得し、それをReduxストアで管理します。

// dataSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// API呼び出しを行う非同期アクション
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!response.ok) {
      throw new Error('Failed to fetch users');
    }
    return await response.json();
  }
);

// ユーザーデータを管理するSlice
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    items: [],
    status: 'idle', // 状態: idle, loading, succeeded, failed
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export default usersSlice.reducer;

Reactコンポーネントでの利用


次に、この非同期アクションをReactコンポーネントで利用し、取得したデータを表示します。

// UsersList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './dataSlice';

const UsersList = () => {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.items);
  const status = useSelector((state) => state.users.status);
  const error = useSelector((state) => state.users.error);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [dispatch, status]);

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

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

export default UsersList;

動作の流れ

  1. 初回レンダリング
    useEffectによってfetchUsersdispatchされ、API呼び出しが開始されます。
  2. 状態の変化
    非同期処理の進行状況に応じて、状態がloadingsucceededまたはfailedに変化します。
  3. データ表示
    succeededで取得したユーザーリストをUIに反映します。

実行結果


取得したユーザーリストが、以下のように表示されます。

User List
- Leanne Graham
- Ervin Howell
- Clementine Bauch
- ...

このサンプルコードにより、createAsyncThunkを使った基本的なAPI呼び出しの実装方法を学べます。これを応用すれば、さまざまなデータ取得機能を効率的に構築できます。

エラーハンドリングの実装方法

エラーハンドリングの重要性


API呼び出しでは、ネットワークエラー、サーバーエラー、データフォーマットの問題などが発生する可能性があります。適切にエラーハンドリングを実装することで、アプリケーションの信頼性を向上させ、ユーザー体験を損なわない仕組みを提供できます。

createAsyncThunkでのエラーハンドリング


createAsyncThunkでは、非同期関数内でエラーが発生するとrejectedアクションが自動的にディスパッチされます。この仕組みを活用し、ReduxストアやUIでエラーを適切に管理します。

エラーをスローする


createAsyncThunk内でエラーをスローすることで、rejected状態をトリガーします。

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, thunkAPI) => {
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      return thunkAPI.rejectWithValue(error.message); // カスタムエラーを返す
    }
  }
);

エラー情報の管理


thunkAPI.rejectWithValueを使用すると、カスタムエラー情報をaction.payloadに格納できます。これにより、エラー内容をより詳細に管理可能です。

リデューサーでのエラー処理


rejectedアクションのペイロードを使用してエラー状態を保存します。

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

UIでのエラー表示


エラー状態に基づいて適切なメッセージをUIに表示します。

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './dataSlice';

const UsersList = () => {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.items);
  const status = useSelector((state) => state.users.status);
  const error = useSelector((state) => state.users.error);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [dispatch, status]);

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

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

export default UsersList;

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

  1. ユーザーに有用なエラーメッセージを表示する
  • ネットワークエラーやタイムアウトなどの具体的な問題を説明します。
  1. 再試行機能を提供する
  • ボタンなどを使って再度APIを呼び出せる仕組みを追加します。
  1. エラーをログに記録する
  • エラーをログやモニタリングツールに送信して、運用中の問題を迅速に検知します。

まとめ


適切なエラーハンドリングは、アプリケーションの堅牢性を高め、ユーザー体験を向上させます。createAsyncThunkを活用してエラーを効率的に管理し、信頼性の高いアプリケーションを構築しましょう。

状態管理とUIの連携

非同期処理の状態をUIに反映する


Redux ToolkitのcreateAsyncThunkを使用すると、非同期処理の状態(pending, fulfilled, rejected)を効率的に管理できます。これらの状態をUIに反映することで、ユーザーに処理の進行状況をわかりやすく伝えることが可能です。

状態とUIの基本連携


以下のステータスに応じて、UIを動的に更新します:

  • loading(pending): ローディングスピナーや「読み込み中」の表示を行う。
  • succeeded(fulfilled): 取得したデータを表示する。
  • failed(rejected): エラーメッセージを表示し、再試行ボタンを提供する。

サンプルコード


以下は、ユーザーリストを取得する非同期処理の状態をUIに反映する例です。

// UsersList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './dataSlice';

const UsersList = () => {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.items);
  const status = useSelector((state) => state.users.status);
  const error = useSelector((state) => state.users.error);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [dispatch, status]);

  // 状態に応じたUIの出力
  if (status === 'loading') {
    return <p>Loading...</p>;
  }

  if (status === 'failed') {
    return (
      <div>
        <p>Error: {error}</p>
        <button onClick={() => dispatch(fetchUsers())}>Retry</button>
      </div>
    );
  }

  if (status === 'succeeded' && users.length === 0) {
    return <p>No users found.</p>;
  }

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

export default UsersList;

UI更新のポイント

  1. ステータスによる条件分岐
    状態(loading, succeeded, failed)ごとに適切なコンポーネントを表示します。
  2. エラーメッセージの表示
    rejected状態では、エラーメッセージを表示し、ユーザーが次に行うべき操作(再試行など)を案内します。
  3. データが空の場合の対応
    データが存在しない場合(succeededかつリストが空)に、ユーザーに「データがありません」と明示します。

ローディングスピナーのカスタマイズ例


ローディング中に表示するスピナーをカスタマイズすることもできます。

import React from 'react';

const Spinner = () => {
  return (
    <div className="spinner">
      <div className="double-bounce1"></div>
      <div className="double-bounce2"></div>
    </div>
  );
};

export default Spinner;

CSS例:

.spinner {
  width: 40px;
  height: 40px;
  position: relative;
}

.double-bounce1, .double-bounce2 {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: #67c2f1;
  opacity: 0.6;
  position: absolute;
  top: 0;
  left: 0;
  animation: bounce 2.0s infinite ease-in-out;
}

@keyframes bounce {
  0%, 100% {
    transform: scale(0.0);
  }
  50% {
    transform: scale(1.0);
  }
}

ベストプラクティス

  • ユーザーに処理状況を明確に伝える
    ステータスごとにUIを切り替え、処理の進行やエラー状況を明示する。
  • アクセシビリティ対応
    ローディングやエラー表示時に、スクリーンリーダー対応のテキストを追加する。
  • 再試行機能の提供
    ユーザーがエラー後に簡単に再試行できるUIを提供する。

まとめ


非同期処理の状態をUIに適切に反映させることで、ユーザーにとって使いやすく、直感的なアプリケーションを構築できます。Redux ToolkitのcreateAsyncThunkを活用して、効率的な状態管理を実現しましょう。

createAsyncThunkの高度な利用方法

複数のAPI呼び出しを連携させる


複雑な非同期処理では、複数のAPI呼び出しを連携させる必要があります。createAsyncThunkを活用して、依存関係を考慮した非同期処理を簡潔に実装できます。

例: 依存関係のあるAPI呼び出し


以下の例では、まずユーザーリストを取得し、その後に各ユーザーの詳細情報を取得します。

import { 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('Failed to fetch users');
  }
  return await response.json();
});

// 各ユーザーの詳細を取得
export const fetchUserDetails = createAsyncThunk(
  'users/fetchUserDetails',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    if (!response.ok) {
      throw new Error(`Failed to fetch details for user ${userId}`);
    }
    return await response.json();
  }
);

// コンポーネントでの利用例
const fetchUserData = async (dispatch, userId) => {
  await dispatch(fetchUsers()); // ユーザーリストを取得
  await dispatch(fetchUserDetails(userId)); // 特定のユーザー詳細を取得
};

パラメータの動的な取り扱い


API呼び出しに必要なパラメータを、動的にcreateAsyncThunkに渡すことができます。

export const fetchFilteredData = createAsyncThunk(
  'data/fetchFilteredData',
  async (filterOptions, thunkAPI) => {
    const query = new URLSearchParams(filterOptions).toString();
    const response = await fetch(`https://api.example.com/data?${query}`);
    if (!response.ok) {
      throw new Error('Failed to fetch filtered data');
    }
    return await response.json();
  }
);

// コンポーネントでの使用例
dispatch(fetchFilteredData({ category: 'books', limit: 10 }));

並列処理の実装


複数の非同期処理を並列で実行したい場合、Promise.allを利用して効率的に処理を進められます。

export const fetchAllData = createAsyncThunk(
  'data/fetchAllData',
  async (_, thunkAPI) => {
    const [usersResponse, postsResponse] = await Promise.all([
      fetch('https://jsonplaceholder.typicode.com/users'),
      fetch('https://jsonplaceholder.typicode.com/posts'),
    ]);

    if (!usersResponse.ok || !postsResponse.ok) {
      throw new Error('Failed to fetch data');
    }

    const users = await usersResponse.json();
    const posts = await postsResponse.json();

    return { users, posts };
  }
);

// 使用例
dispatch(fetchAllData());

カスタムエラーハンドリング


デフォルトのエラーハンドリングに加え、カスタム処理を組み込むことでエラーへの対応を強化します。

export const fetchDataWithErrorHandling = createAsyncThunk(
  'data/fetchDataWithErrorHandling',
  async (_, thunkAPI) => {
    try {
      const response = await fetch('https://api.example.com/data');
      if (!response.ok) {
        throw new Error('Custom error message');
      }
      return await response.json();
    } catch (error) {
      return thunkAPI.rejectWithValue({ message: error.message, code: error.code });
    }
  }
);

// リデューサーでの処理
extraReducers: (builder) => {
  builder
    .addCase(fetchDataWithErrorHandling.rejected, (state, action) => {
      state.error = action.payload || 'An unknown error occurred';
    });
};

非同期処理をキャンセルする


長時間のAPI呼び出しをキャンセルするために、AbortControllerを活用します。

export const fetchDataWithCancel = createAsyncThunk(
  'data/fetchDataWithCancel',
  async (_, thunkAPI) => {
    const controller = new AbortController();
    thunkAPI.signal.addEventListener('abort', () => controller.abort());

    const response = await fetch('https://api.example.com/data', {
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }

    return await response.json();
  }
);

// 使用例
const thunk = dispatch(fetchDataWithCancel());
// 必要に応じてキャンセル
thunk.abort();

まとめ


createAsyncThunkは柔軟な非同期処理をサポートし、複雑な要件にも対応できます。パラメータ管理や複数APIの呼び出し、並列処理、キャンセル処理などを活用して、高度な状態管理を効率的に行いましょう。

テストとデバッグのポイント

createAsyncThunkを利用したコードのテスト


createAsyncThunkで作成した非同期処理のテストでは、以下の点を重視します:

  • 正しいアクションがディスパッチされるかを確認する。
  • API呼び出しの成功および失敗時の挙動をテストする。
  • 状態が期待通りに更新されるかを検証する。

モックAPIを使用したテスト


API呼び出しをモックすることで、実際のネットワーク通信を行わずに非同期処理をテストできます。

import { fetchUsers } from './dataSlice';
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './dataSlice';

describe('fetchUsers async thunk', () => {
  let store;

  beforeEach(() => {
    store = configureStore({
      reducer: { users: usersReducer },
    });
  });

  it('dispatches fulfilled on successful API call', async () => {
    // モックAPIレスポンス
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve([{ id: 1, name: 'John Doe' }]),
      })
    );

    await store.dispatch(fetchUsers());
    const state = store.getState().users;

    expect(state.items).toEqual([{ id: 1, name: 'John Doe' }]);
    expect(state.status).toBe('succeeded');
  });

  it('dispatches rejected on API error', async () => {
    // モックAPIエラー
    global.fetch = jest.fn(() => Promise.reject(new Error('Network error')));

    await store.dispatch(fetchUsers());
    const state = store.getState().users;

    expect(state.status).toBe('failed');
    expect(state.error).toBe('Network error');
  });
});

Redux DevToolsでのデバッグ


Redux DevToolsは、非同期処理を含むReduxアプリケーションの動作を視覚化する便利なツールです。以下の点を確認できます:

  • 各アクションの流れ(pending, fulfilled, rejected
  • 状態の変化
  • ディスパッチされたアクションのペイロード

デバッグ中に確認すべきポイント

  1. アクション名が正しいか
    非同期アクションが期待通りにディスパッチされているか確認します。
  2. ペイロードの内容
    アクションが正しいデータを含んでいるか確認します。
  3. 状態更新の正確性
    状態が正しく変化しているか(例えば、loadingsucceeded)を追跡します。

エラー処理のデバッグ


非同期処理に関連するエラーのデバッグ方法として、以下を活用します:

  • try-catchブロック: 非同期関数内でエラーをキャッチしてログを出力します。
  • thunkAPI.rejectWithValue: カスタムエラー情報を返し、問題の特定を容易にします。
  • コンソールログ: 非同期処理中の重要なデータを出力して問題箇所を確認します。
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, thunkAPI) => {
    try {
      const response = await fetch('https://api.example.com/users');
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('Fetch users failed:', error);
      return thunkAPI.rejectWithValue(error.message);
    }
  }
);

パフォーマンスの最適化


デバッグの際にパフォーマンスが問題となる場合は以下を確認します:

  • 不要な再レンダリング: React.memouseMemoを活用してパフォーマンスを向上させます。
  • API呼び出しの頻度: キャッシュや状態の事前確認を行い、不要なAPI呼び出しを避けます。

まとめ


createAsyncThunkを利用した非同期処理のテストとデバッグを適切に行うことで、アプリケーションの信頼性を向上させます。モックAPIやRedux DevToolsを活用して効率的に問題を特定し、必要に応じて状態更新やエラーハンドリングを最適化しましょう。

まとめ


本記事では、Redux ToolkitのcreateAsyncThunkを使ったAPI呼び出しの実装方法について詳しく解説しました。createAsyncThunkは、非同期処理のライフサイクル(pending, fulfilled, rejected)を簡単に管理し、効率的な状態管理を実現します。

基本構文やエラーハンドリング、UIとの連携、複数APIの呼び出し、そしてテストとデバッグのポイントまで幅広く取り上げました。これにより、実際のアプリケーション開発で直面する多くの課題を解決できるはずです。

createAsyncThunkを活用して、堅牢でスケーラブルなReactアプリケーションを構築し、ユーザー体験を向上させましょう。

コメント

コメントする

目次