Redux ToolkitでAPI通信と状態管理をする方法!createAsyncThunkの使い方を解説!

ReactでWebアプリケーションを開発していると、APIから取得したデータを複数のコンポーネントで使用したいことがあります。

例えば、APIからユーザー情報を取得して画面に表示する場合、取得したデータだけでなく、API通信中なのか、API通信が成功したのか、エラーが発生したのかといった状態も管理する必要があります。

このようなAPI通信と状態管理には、Redux ToolkitのcreateAsyncThunkcreateSliceを使用できます。

この記事では、Redux Toolkitを使ってAPIからデータを取得し、取得したデータや通信状態をRedux Storeで管理する方法を、シンプルなサンプルコードでわかりやすく解説します。

Redux Toolkitとは?

Redux Toolkitとは、Reduxをより簡単に使用するための公式ツールセットです。

Reduxでは、アプリケーションで使用するデータをStoreと呼ばれる場所でまとめて管理できます。Redux Toolkitを使用すると、createSliceなどを使ってReduxの状態や状態を変更する処理を比較的少ないコードで記述できます。

また、createAsyncThunkを使用すると、API通信などの非同期処理もReduxと組み合わせて管理できます。

今回の記事では、次のような流れでユーザー情報を取得します。

Reactコンポーネント
        ↓
createAsyncThunk
        ↓
APIへリクエスト
        ↓
取得したデータ
        ↓
Redux Storeへ保存
        ↓
Reactコンポーネントに表示

必要なパッケージをインストールする

ReactプロジェクトでRedux Toolkitを使用するには、@reduxjs/toolkitreact-reduxをインストールします。例えば、次のコマンドを実行します。

npm install @reduxjs/toolkit react-redux

@reduxjs/toolkitにはRedux Toolkitの機能が含まれており、react-reduxはReactからRedux Storeを利用するために使用します。

Redux ToolkitでAPI通信する

今回は、サンプルとしてJSONPlaceholderのAPIからユーザー情報を1件取得します。使用するAPIはhttps://jsonplaceholder.typicode.com/users/1です。

このAPIから、おおよそ次のようなユーザー情報を取得できます。

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

このAPIから取得したユーザー情報全体をRedux Storeへ保存し、その中からnameemailをReactの画面に表示してみましょう。

createAsyncThunkでAPI通信を作成する

まず、createAsyncThunkを使用してAPI通信を作成します。userSlice.jsを作成し、次のように記述します。

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

export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async () => {
    const response = await fetch(
      'https://jsonplaceholder.typicode.com/users/1'
    );

    if (!response.ok) {
      throw new Error('ユーザー情報の取得に失敗しました');
    }

    const data = await response.json();

    return data;
  }
);

createAsyncThunkは、API通信などの非同期処理をReduxで扱うために使用できます。上記のサンプルコードでは、fetch()を使用してAPIへリクエストし、取得したデータをreturn dataで返しています。

この返したデータは、API通信が成功したときにRedux Storeへ保存できます。

createAsyncThunkを使用すると、非同期処理の状態に応じて、pendingfulfilledrejectedに対応するActionが自動的に生成されます。

状態意味
pendingAPI通信中
fulfilledAPI通信成功
rejectedAPI通信失敗

この3つを利用することで、API通信中・成功・失敗の状態を管理できます。

createSliceでAPIの状態を管理する

続いて、同じuserSlice.jscreateSliceを記述します。

const userSlice = createSlice({
  name: 'user',

  initialState: {
    userData: null,
    loading: false,
    error: null,
  },

  reducers: {},

  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.userData = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export const userReducer = userSlice.reducer;

今回のRedux Storeでは、userDataloadingerrorの3つの状態を管理しています。

initialState: {
  userData: null,
  loading: false,
  error: null,
}
状態内容
userDataAPIから取得したユーザー情報
loadingAPI通信中かどうか
errorAPI通信で発生したエラー

また、fetchUser.pendingfetchUser.fulfilledfetchUser.rejectedfetchUserは、先ほどcreateAsyncThunkで定義したfetchUserです。

export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async () => {
    // 非同期処理
  }
);

そのため、extraReducersでは次のようにfetchUser.pendingなどを指定して、それぞれの状態に応じた処理を記述できます。

.addCase(fetchUser.pending, ...)
.addCase(fetchUser.fulfilled, ...)
.addCase(fetchUser.rejected, ...)

つまり、fetchUserはAPI通信そのものを表すThunkであり、fetchUser.pendingなどを使うことで、そのAPI通信の開始・成功・失敗に応じた処理を指定できます。

API通信中のpending

API通信が開始されると、fetchUser.pendingのActionがdispatchされ、対応する処理が実行されます。

.addCase(fetchUser.pending, (state) => {
  state.loading = true;
  state.error = null;
})

ここではloadingtrueにしています。これによって、React側で「読み込み中です」といった表示ができます。

API通信成功時のfulfilled

API通信が正常に完了すると、fetchUser.fulfilledのActionがdispatchされ、対応する処理が実行されます。

.addCase(fetchUser.fulfilled, (state, action) => {
  state.loading = false;
  state.userData = action.payload;
})

createAsyncThunkの処理が成功し、returnしたデータは、fulfilled側のaction.payloadに格納されます。そのため、state.userData = action.payloadとすることで、取得したユーザー情報をRedux Storeへ保存できます。API通信は完了しているため、loadingfalseに戻します。

API通信失敗時のrejected

API通信でエラーが発生すると、fetchUser.rejectedのActionがdispatchされ、対応する処理が実行されます。

.addCase(fetchUser.rejected, (state, action) => {
  state.loading = false;
  state.error = action.error.message;
});

ここでは、エラー内容をerrorへ保存しています。このように、createAsyncThunkextraReducersを組み合わせることで、API通信の状態をRedux Storeで管理できます。

Redux Storeを作成する

次に、store.jsを作成します。

import { configureStore } from '@reduxjs/toolkit';
import { userReducer } from './userSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

configureStoreを使用してRedux Storeを作成し、先ほど作成したuserSliceのReducerを登録しています。

ここで使用しているuserReducerは、userSlice.jsで次のように定義したuserReducerと同じものです。

export const userReducer = userSlice.reducer;

store.jsでは、これを次のようにimportして使用しています。

import { userReducer } from './userSlice';

一方、次のuserはRedux Store上で使用する状態の名前です。

reducer: {
  user: userReducer,
}

そのため、Redux Storeでは次のような形で状態が管理されます。

{
  user: {
    userData: null,
    loading: false,
    error: null
  }
}

つまり、userReduceruserSlice.jsで定義したReducerの名前で、userはRedux Store上でその状態を管理するための名前です。

Providerを設定する

作成したRedux StoreをReactから利用できるように、アプリケーションをProviderで囲みます。例えば、main.jsxを次のようにします。

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

Providerstoreを渡すことで、その配下にあるReactコンポーネントからRedux Storeを利用できるようになります。

ReactからAPIを呼び出す

最後に、ReactコンポーネントからAPI通信を実行します。App.jsxを次のようにします。

import { useDispatch, useSelector } from 'react-redux';
import { fetchUser } from './userSlice';

function App() {
  const dispatch = useDispatch();

  const { userData, loading, error } = useSelector(
    (state) => state.user
  );

  const handleClick = () => {
    dispatch(fetchUser());
  };

  return (
    <div>
      <button onClick={handleClick}>
        ユーザー情報を取得
      </button>

      {loading && <p>読み込み中です...</p>}

      {error && <p>{error}</p>}

      {userData && (
        <div>
          <p>名前:{userData.name}</p>
          <p>メールアドレス:{userData.email}</p>
        </div>
      )}
    </div>
  );
}

export default App;

useDispatchは、Reduxへ処理を実行するよう通知するために使用します。

今回使用しているfetchUserは、先ほどuserSlice.jscreateAsyncThunkを使って定義したfetchUserと同じものです。

export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async () => {
    // API通信
  }
);

App.jsxでは、このfetchUserを次のようにimportしています。

import { fetchUser } from './userSlice';

そのため、ボタンをクリックしてdispatch(fetchUser())が実行されると、userSlice.jsで定義したAPI通信が開始されます。

一方、Redux Storeの状態を取得するために使用しているのがuseSelectorです。

const { userData, loading, error } = useSelector(
  (state) => state.user
);

ここで使用しているstate.useruserは、store.jsでReducerを登録したときのuserと同じ名前です。

reducer: {
  user: userReducer,
}

さらに、const { userData, loading, error } = ...の部分では、state.userの中にあるuserDataloadingerrorを分割代入しています。

これによって、Redux Storeで管理しているuserDataloadingerrorをReactコンポーネントから利用できます。

Redux Toolkitで出てくる似た名前の関係

Redux Toolkitのコードでは、userSlicename: 'user'fetchUserstate.userなど、似た名前が多く登場します。

初心者の場合、「全部同じ名前にしないといけないの?」と混乱しやすいですが、実際には、自由に決められる名前と、他の場所と対応している名前があります。

名前役割他の名前との関係
userSlicecreateSlice()の結果を格納するJavaScriptの変数名基本的に自由。
name: 'user'ならuserSliceのようにSlice名に合わせた変数名にすることが多い。
name: 'user'Sliceの名前基本的に自由。
reducer: { user: userReducer }userと同じ名前にすることが多い
'user/fetchUser'createAsyncThunkで使用するAction Type基本的に自由。
userはSlice名、fetchUserはThunkの変数名と合わせることが多い。
fetchUsercreateAsyncThunkで作成したThunkを格納する変数名createAsyncThunkで定義したfetchUserを、fetchUser.pendingfetchUser.fulfilledfetchUser.rejecteddispatch(fetchUser())などで同じ名前として参照する
userReduceruserSlice.reducerを格納した変数名store.jsでimportして使用する場合は同じ名前で参照する。
例えば、export const userReducer = userSlice.reducer;と定義した場合は、import { userReducer } from './userSlice';とする
reducer: { user: userReducer }userRedux Store内の状態のキーuseSelector((state) => state.user)userと一致する

Redux Toolkitで似た名前が多く登場した場合は、「この名前はどこから決まっているのか」を1つずつ確認すると理解しやすくなります。

API通信から画面表示までの流れ

今回のコードでは、ユーザーがボタンをクリックしてから画面にユーザー情報が表示されるまで、次のように処理されます。

ボタンをクリック
        ↓
dispatch(fetchUser())
        ↓
fetchUser.pending
        ↓
loading = true
        ↓
APIへリクエスト
        ↓
通信成功
        ↓
fetchUser.fulfilled
        ↓
userDataに取得データを保存
loading = false
        ↓
Reactの画面が更新される

API通信に失敗した場合は、fulfilledではなくrejectedが実行されます。

API通信失敗
      ↓
fetchUser.rejected
      ↓
errorにエラー内容を保存
loading = false
      ↓
画面にエラーを表示

このように考えると、Redux Toolkitを使ったAPI通信では、APIを呼び出す処理だけでなく、その前後の状態もRedux Storeで管理していることが分かります。

createAsyncThunkとcreateSliceの役割

ここまでの内容を整理すると、それぞれの主な役割は次のようになります。

機能主な役割
createAsyncThunkAPI通信などの非同期処理をReduxで扱う
createSliceReduxの状態と状態更新処理を定義する
configureStoreRedux Storeを作成する
useDispatchReduxの処理を実行する
useSelectorRedux Storeの状態を取得する

特に初心者の場合は、まずcreateAsyncThunkがAPI通信などの非同期処理をReduxで扱う役割を担当し、createSliceがその処理結果や通信状態を管理すると考えると理解しやすいでしょう。

本記事のまとめ

この記事では「Redux Toolkitを使ったAPI通信と状態管理」について、以下の内容を説明しました。

  • createAsyncThunkを使用するとAPI通信などの非同期処理を作成できる
  • pendingfulfilledrejectedを使って通信状態を管理できる
  • createSliceで取得データ・読み込み状態・エラーを管理できる
  • configureStoreのキーがstate.xxxの名前になる
  • 似た名前でも、役割によって一致が必要なものと自由に決められるものがある

お読みいただきありがとうございました。

スポンサーリンク