ReactでWebアプリケーションを開発していると、APIから取得したデータを複数のコンポーネントで使用したいことがあります。
例えば、APIからユーザー情報を取得して画面に表示する場合、取得したデータだけでなく、API通信中なのか、API通信が成功したのか、エラーが発生したのかといった状態も管理する必要があります。
このようなAPI通信と状態管理には、Redux ToolkitのcreateAsyncThunkとcreateSliceを使用できます。
この記事では、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/toolkitとreact-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へ保存し、その中からnameとemailを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を使用すると、非同期処理の状態に応じて、pending、fulfilled、rejectedに対応するActionが自動的に生成されます。
| 状態 | 意味 |
pending | API通信中 |
fulfilled | API通信成功 |
rejected | API通信失敗 |
この3つを利用することで、API通信中・成功・失敗の状態を管理できます。
createSliceでAPIの状態を管理する
続いて、同じuserSlice.jsにcreateSliceを記述します。
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では、userData、loading、errorの3つの状態を管理しています。
initialState: {
userData: null,
loading: false,
error: null,
}| 状態 | 内容 |
userData | APIから取得したユーザー情報 |
loading | API通信中かどうか |
error | API通信で発生したエラー |
また、fetchUser.pending、fetchUser.fulfilled、fetchUser.rejectedのfetchUserは、先ほど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;
})ここではloadingをtrueにしています。これによって、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通信は完了しているため、loadingはfalseに戻します。
API通信失敗時のrejected
API通信でエラーが発生すると、fetchUser.rejectedのActionがdispatchされ、対応する処理が実行されます。
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});ここでは、エラー内容をerrorへ保存しています。このように、createAsyncThunkとextraReducersを組み合わせることで、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
}
}つまり、userReducerはuserSlice.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>
);Providerにstoreを渡すことで、その配下にある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.jsでcreateAsyncThunkを使って定義した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.userのuserは、store.jsでReducerを登録したときのuserと同じ名前です。
reducer: {
user: userReducer,
}さらに、const { userData, loading, error } = ...の部分では、state.userの中にあるuserData、loading、errorを分割代入しています。
これによって、Redux Storeで管理しているuserData、loading、errorをReactコンポーネントから利用できます。
Redux Toolkitで出てくる似た名前の関係
Redux Toolkitのコードでは、userSlice、name: 'user'、fetchUser、state.userなど、似た名前が多く登場します。
初心者の場合、「全部同じ名前にしないといけないの?」と混乱しやすいですが、実際には、自由に決められる名前と、他の場所と対応している名前があります。
| 名前 | 役割 | 他の名前との関係 |
|---|---|---|
userSlice | createSlice()の結果を格納するJavaScriptの変数名 | 基本的に自由。name: 'user'ならuserSliceのようにSlice名に合わせた変数名にすることが多い。 |
name: 'user' | Sliceの名前 | 基本的に自由。reducer: { user: userReducer }のuserと同じ名前にすることが多い |
'user/fetchUser' | createAsyncThunkで使用するAction Type | 基本的に自由。userはSlice名、fetchUserはThunkの変数名と合わせることが多い。 |
fetchUser | createAsyncThunkで作成したThunkを格納する変数名 | createAsyncThunkで定義したfetchUserを、fetchUser.pending、fetchUser.fulfilled、fetchUser.rejected、dispatch(fetchUser())などで同じ名前として参照する |
userReducer | userSlice.reducerを格納した変数名 | store.jsでimportして使用する場合は同じ名前で参照する。例えば、 export const userReducer = userSlice.reducer;と定義した場合は、import { userReducer } from './userSlice';とする |
reducer: { user: userReducer }のuser | Redux 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の役割
ここまでの内容を整理すると、それぞれの主な役割は次のようになります。
| 機能 | 主な役割 |
createAsyncThunk | API通信などの非同期処理をReduxで扱う |
createSlice | Reduxの状態と状態更新処理を定義する |
configureStore | Redux Storeを作成する |
useDispatch | Reduxの処理を実行する |
useSelector | Redux Storeの状態を取得する |
特に初心者の場合は、まずcreateAsyncThunkがAPI通信などの非同期処理をReduxで扱う役割を担当し、createSliceがその処理結果や通信状態を管理すると考えると理解しやすいでしょう。
本記事のまとめ
この記事では「Redux Toolkitを使ったAPI通信と状態管理」について、以下の内容を説明しました。
createAsyncThunkを使用するとAPI通信などの非同期処理を作成できるpending・fulfilled・rejectedを使って通信状態を管理できるcreateSliceで取得データ・読み込み状態・エラーを管理できるconfigureStoreのキーがstate.xxxの名前になる- 似た名前でも、役割によって一致が必要なものと自由に決められるものがある
お読みいただきありがとうございました。