Redux入門者向け初めてのRedux ToolkitとRedux Thunkの非同期処理
Reduxの基礎として”React初心者でも読めば必ずわかるReactのRedux講座”を公開していますが本文書ではRedux Toolkitの使用方法について説明を行っています。
Reduxは難解だとイメージを持っている人も多いと思うので可能な限りシンプルなコードを利用しているので安心して読み進めることができます。
前半はRedux Toolkitを理解するためにRedux Toolkitを利用しない場合のカウンターのコードを記述した後にRedux Toolkitをインストールを行いRedux Toolkitを利用したコードに書き換えていきます。
後半にはRedux Toolkitにおける非同期処理についての説明も行っています。Redux ToolkitだけではなくRedux ToolkitにおけるRedux Thunkの方法方法も一緒に理解することができます。
最後にTypeScriptでカウンターのコードを記述しています。
目次
Redux Toolkitとは
Reduxは複数のコンポーネントからアクセス可能なデータを一箇所で一元管理するためのライブラリです。Redux Toolkitの利用は必須ではなくReduxのみ利用しても同じアプリケーションを開発することは可能です。ではなぜRedux Toolkitが必要になるのでしょう?
Reduxのみでは以下のような問題点を抱えていると言われています。
- Redux Storeを構成することが複雑すぎる
- 大規模のアプリケーションを構築するにはたくさんの追加パッケージをインストールする必要がある
- Reduxを利用するためには定型文的なコードを大量に記述する必要がある
上記の問題点を解消することを目的に開発されたライブラリがRedux Toolkitです。Reduxの公式ホームページではReduxを利用する際にRedux Toolkitを利用することを推奨しています。
Redux Toolkitを利用することで定型文的なコード量を減らすことができる上、開発者が陥りがちな問題を避けるためにこれまで蓄積されたベストプラクティスが反映されているためReduxのコードを効率よく記述できるようになっています。
Reactプロジェクトの作成
Redux Toolkitの動作確認を行うためにnpx create-react-appコマンドでReactプロジェクトの作成を行います。プロジェクト名にredux-toolkit-beginnerとしていますが任意の名前をつけることができるので好きな名前をつけてください。
% npx create-react-app redux-toolkit-beginner
Reduxを利用しないカウンターの作成
プロジェクトフォルダ作成後App.jsを開いてReduxを利用しない場合のカウンターのコードを記述します。
useState Hookでcount変数を定義し、ボタンを2つ用意します。片方のボタンをクリックするとcountの値が1増え、もう一方のボタンをクリックするとcountの値が減ります。
import './App.css';
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Up</button>
<button onClick={() => setCount(count - 1)}>Down</button>
</div>
);
}
export default App;
npm startコマンドを実行してlocalhost:3000にアクセスすると以下の画面が表示されます。ボタンをクリックしてcountの数が変わることを確認してください。
Reduxライブラリのインストール
Reduxとredux toolkitパッケージのインストールを行います。
$ npm install @reduxjs/toolkit react-redux
Redux Toolkitの設定
Storeの作成
Reduxではすべてのコンポーネントからアクセス可能なstoreと呼ばれる場所を作成する必要があります。srcフォルダにreduxフォルダを作成しその中にstore.jsファイルを作成します。redux, store.jsという名前をつけていますがフォルダ名もファイル名も任意の名前をつけることができます。
store.jsファイルの中ではStoreを作成するために記述するコードhttps://redux-toolkit.js.org/tutorials/quick-startを参考に記述しています。
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {},
});
Providerコンポーネントの設定
store.jsファイル作成後、作成したstoreにすべてのコンポーネントからアクセスできるようにindex.jsファイルを更新します。react-reduxからimportしたProviderでAppコンポーネントを包みます。さらに作成したstoreをimportしてpropsとしてProviderコンポーネントに渡します。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Sliceファイルの作成
カウンターの変数count、countの初期値やcountの値を更新する関数をRedux ToolkitではSliceファイルに追加していきます。Sliceファイルは管理したいデータを目的別/機能別に分けます。ここではカウンターの変数countに関する設定をSliceファイルであるcounterSlice.jsファイルにすべて設定します。もしユーザ情報をRedux Toolkitで管理したい場合はuserSlice.jsといった名前の別ファイルを作成しユーザ情報の初期値や更新に利用する関数を記述します。Sliceでデータを目的別にわけることでデータ管理の混乱を避け効率的に管理することができます。Sliceファイルの中で初期値、reducer、Action creatorsを設定することができるためそれぞれの処理を複数のファイルに分けて記述する必要がありません。
reduxフォルダの中にcounterSlice.jsファイルを作成し、Redux ToolkitからcreateSlice関数をimportします。createSliceの引数にはname, initialState, reducersプロパティを持つオブジェクトを指定します。
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 名前,
initialState: {
初期値
},
reducers: {
関数
},
});
nameにはSliceを識別するための名前を設定します。initialStateには共有するデータ(state)の初期値を設定し、reducersの中にはstateを更新するための関数を設定します。
下記のコードではsliceの名前をcounter、countの初期値を0に設定し、reducersの中でincrease関数とdecrease関数を定義しています。increase関数では引数にstateが入りcountの値を1増やす処理を行います。decrease関数では引数にstateが入りcountの値を1減らす処理を行います。
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0,
},
reducers: {
increase: (state) => {
state.count += 1;
},
decrease: (state) => {
state.count -= 1;
},
},
});
reducersの中で設定した関数increase, decreaseはredux Toolkitでは自動で同名のAction creatorsを作成します。Action creatorsを後ほど出てくるdispatchで指定するためexportを行い他のコンポーネントからimportできるようにします。
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0,
},
reducers: {
increase: (state) => {
state.count += 1;
},
decrease: (state) => {
state.count -= 1;
},
},
});
export const { increase, decrease } = counterSlice.actions;
export default counterSlice.reducer;
StoreへのSliceの追加
作成したcounterSliceはstore.jsのstoreに登録する必要があります。toolkitからimportしたconfigureStore関数の引数に設定するオブジェクトのreducerプロパティに追加します。
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
これでRedux Toolkitの設定は完了です。ここからは設定したstateをAppコンポーネントで利用する方法を確認していきます。
useSelectorの設定
useSelector Hookを利用することでcounterSliceで設定したcountの値を取得することができます。stateのドットの直後に指定しているcounterはstore.jsのreducerに設定したオブジェクトのプロパティのcounterに対応します。counterSlice.jsファイルのnameで設定した”counter”ではありません。countの値を取得したい場合はstate.countではなくstate.counter.countであることを注意してください。
import './App.css';
import { useSelector } from 'react-redux';
function App() {
const count = useSelector((state) => state.counter.count);
return (
<div className="App">
<h1>Count: {count}</h1>
</div>
);
}
export default App;
counterSliceに設定したcountを表示させるだけであればuseSelectorを利用するだけで完了です。
useDispatchの設定
countの値を更新するためにはAppコンポーネントでAction creatorsを呼び出す必要があります。Action creatorsを実行するためにはdispatchが必要となるためuseDispatchをimportします。Action creatorsはcounterSlice.jsファイルでexportしているのでincreate, decreaseをAppコンポーネントでimportします。
import './App.css';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
function App() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
</div>
);
}
export default App;
Redux Toolkitを設定後のカウンターの画面です。Reduxを利用しない場合と同様にボタンによってCountの値を増減させることができます。
Reduxを利用しないコードからRedux Toolkitへ書き換え作業は完了です。Reduxを利用しないコートでは他のコンポーネントでcountにアクセスするためにはpropsを利用することができますがReduxを利用している場合はindex.jsのProviderで包んでいるコンポーネント以下にあるコンポーネントからであればどこからでもアクセスすることができます。その時にはuseSelector Hookでアクセスを行い、useDispatch Hookを利用して更新を行います。
Redux DevToolsでの確認
Chromeブラウザを利用している場合はExtentionsのRedux DevToolsをインストールしていればRedux Toolkitを利用して設定したデータの状態を確認することができます。ブラウザのデベロッパーツールを開いてReduxタブを選択することで下記の画面が表示されます。
UpボタンをクリックするとCounterSlice.jsのreducersで設定したnameの値とACTIONの名前が左側のパネルに表示されます。右側のパネルのStateタブでは現在のcountの値を確認することができます。Redux DevToolsを見ることで現在のstateやACTIONが設定通りに動作しているか確認することができます。
counterSlice.jsのnameをcounterからcounter_1に変更するとRedux Devtoolsの名前も変わっていることが確認できます。
export const counterSlice = createSlice({
name: 'counter_1',
//略
Actionタブを見るとtypeが”counter_1/increate"になっていることが確認できます。
非同期処理の記述方法
ここでは3つの異なる方法で非同期処理のコードを記述していきます。1つ目はRedux Thunkを利用しない方法、2つ目はRedux Thunkを利用した方法、3つ目はcreateAsyncThunkを利用した方法です。
非同期処理を行うために外部リソースにJSONPLACEHolderを利用します。https://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報を取得することができます。
Redux Thunkを利用しない場合
カウンターとは別にユーザ情報を管理するためreduxフォルダの中にuserSlice.jsファイルを作成します。createSliceで設定するname, initialState, reducersの引数は下記のように設定を行います。
import { createSlice } from '@reduxjs/toolkit';
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
},
reducers: {
setUsers: (state, action) => {
state.users = action.payload;
},
},
});
export const { setUsers } = usersSlice.actions;
export default usersSlice.reducer;
nameにはusersを設定し、共有するデータusersの初期値は空の配列を設定しています。reducersのsetUsers関数ではdispatchから受け取るpayloadをusersに設定し、自動で作成されるAction CreatorsのsetUsersをexportします。
App.jsファイルではuseEffect Hookを利用してマウント時にJSONSPLACEHolderにアクセスを行いfetch関数を利用してユーザの一覧を取得します。axiosをインストールすればaxiosを利用することも可能です。
import './App.css';
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
import { setUsers } from './redux/usersSlice';
function App() {
const count = useSelector((state) => state.counter.count);
const { users } = useSelector((state) => state.users);
const dispatch = useDispatch();
useEffect(() => {
const getPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await res.json();
dispatch(setUsers(data));
};
getPosts();
}, [dispatch]);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
<h2>User</h2>
{users && users.map((user, index) => <div key={index}>{user.name}</div>)}
</div>
);
}
export default App;
ブラウザ上に表示するユーザ情報usersはuseSelector Hookを使ってstoreから取得します。useEffect内で取得したデータをdispatchで指定したAction CreatorsのsetUsersを使ってstoreに渡しています。
コード記述後にブラウザからアクセスを行い、ユーザ一覧がブラウザ上に表示されることを確認してください。
Redux Thunkを利用した場合
Reduxで非同期処理にRedux Thunkを利用したい場合はパッケージのインストールとミドルウェアの設定が必要になります。Redux Toolkitではパッケージのインストールもミドルウェアの設定も行うことなくRedux Thunkを利用することができます。
Redux Thunkではdispachの引数に非同期処理を含む関数を指定することができます。非同期処理を含む関数はuserSlice.jsファイルの中で定義します。
先にApp.jsファイルの更新を行います。Redux Thunkを利用しない場合はuseEffectの中に非同期関数を記述しdispatchでAction CreatorsのsetUserを指定していましたが今回はuserSlice.jsからgetUsersをimportしuseEffectのdispatchにgetUsersを指定しています。
import './App.css';
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
import { getUsers } from './redux/usersSlice';
function App() {
const count = useSelector((state) => state.counter.count);
const { users } = useSelector((state) => state.users);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUsers());
}, [dispatch]);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
<h2>User</h2>
{users && users.map((user, index) => <div key={index}>{user.name}</div>)}
</div>
);
}
export default App;
UsersSlice.jsファイルにgetUsers関数を追加します。getUsers関数の中では引数にdispatchを持つ関数を戻していることがわかります。getUsers関数の中でdispatchにAction CreatorsのsetUsersを指定しています。Action CreatorsのsetUsersをexportしていますがこの記述がないとエラーが発生します。
import { createSlice } from '@reduxjs/toolkit';
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
},
reducers: {
setUsers: (state, action) => {
state.users = action.payload;
},
},
});
export const getUsers = () => {
return async (dispatch) => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await res.json();
dispatch(setUsers(data));
};
};
export const { setUsers } = usersSlice.actions;
export default usersSlice.reducer;
ブラウザを確認するとRedux Thunkを利用しない場合と同様にユーザの一覧が表示されます。
createAsyncThunkを利用した場合
最後にcreateAsyncThunkを利用した場合の動作確認を行います。createAsyncThunkを利用する場合のApp.jsファイルの内容は同じです。usersSlice.jsからgetUsersをimportしてdispatchの引数にgetUsersを指定しています。
import './App.css';
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
import { getUsers } from './redux/usersSlice';
function App() {
const count = useSelector((state) => state.counter.count);
const { users } = useSelector((state) => state.users);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUsers());
}, [dispatch]);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
<h2>User</h2>
{users && users.map((user, index) => <div key={index}>{user.name}</div>)}
</div>
);
}
export default App;
非同期で外部リソースからデータを取得するcreatAsyncThunk関数はusersSlice.jsファイルの中で利用します。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const getUsers = createAsyncThunk('users/getUsers', async () => {
return await fetch('https://jsonplaceholder.typicode.com/users').then((res) =>
res.json()
);
});
createAsyncThunkでは3つの引数を取ることができますがここでは2つの引数のみ利用しています。1つ目はtype、2つ目はpayloadCreator、3つ目はoptionsです。
先に2つ目のpayloadCreatorですがpayloadCreatorにはpromiseを戻す非同期のcallback関数を指定します。
1つ目のtypeには文字列を設定し設定した文字列によって3つのACTION TYPEが作成されます。ここではTypeにusers/getUsersを設定しているので以下の3つのACTION TYPEが作成されます。createAsyncTypeではこのAction Typeが作成されることが重要です。非同期処理のライフサイクルの中でこれらのAction Typeを使うことで各ライフサイクルで別々の処理を行うことができます。
- pending: ‘users/getUsers/pending’
pending ActionはpayloadCreatorのcallbackが呼ばれる前にdispatchされます。 - fulfilled: ‘users/getUsers/fullfiled’
fullfilled Actionは外部リソースからの情報取得が成功した場合にdispatchされます。 - rejected: ‘users/getUsers/rejected’
外部リソースから取得したデータをusersに保存したい場合はfullfilledを利用してextraReducersとして以下のように設定を行います。
外部リソースから取得したデータをusersに保存したい場合はfullfilledを利用してextraReducersとして以下のように設定を行います。fulfilledでは情報の取得が成功しているのでaction.payloadに入っているデータをusersに設定しています。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const getUsers = createAsyncThunk('users/getUsers', async () => {
return await fetch('https://jsonplaceholder.typicode.com/users').then((res) =>
res.json()
);
});
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
},
extraReducers: {
[getUsers.fulfilled]: (state, action) => {
state.users = action.payload;
},
},
});
export default usersSlice.reducer;
ここまでの設定でブラウザ上にユーザ情報の一覧が表示されます。
fulfilledしか利用していませんでしたが例えばデータ取得中はloadingを表示したいまたエラーが発生したエラー情報を保持したいといった場合にpendingやrejectedを活用することができます。
usersの他に新たににloadingとerrorの変数を追加します。extraReducersの中でfulfilledと同様にpendingとrejectedを追加します。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const getUsers = createAsyncThunk('users/getUsers', async () => {
return await fetch('https://jsonplaceholder.typicode.com/users').then((res) =>
res.json()
);
});
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
loading: false,
error: false,
},
extraReducers: {
[getUsers.pending]: (state) => {
state.loading = true;
},
[getUsers.fulfilled]: (state, action) => {
state.loading = false;
state.users = action.payload;
},
[getUsers.rejected]: (state) => {
state.loading = false;
state.error = true;
},
},
});
export default usersSlice.reducer;
getUsers.pendingでloadingの値をfalseからtrueに変更します。fulfilledとデータ取得に成功するかrejectedでデータ取得に失敗するまではloadingの値はtrueのままになります。データ取得に成功すればloadingの値をfalseに戻し、取得したデータをusersに保存します。データ取得に失敗すればloadingの値をfalseに戻し、errorの値をtrueに変更します。
App.jsファイルでuseSelector Hookから追加したloading, errorを取得します。loadingがtrueの場合は画面に”loading”、errorがtrueの場合は画面に”データ取得に失敗しました”が表示されるように設定します。
import './App.css';
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
import { getUsers } from './redux/usersSlice';
function App() {
const count = useSelector((state) => state.counter.count);
const { users, loading, error } = useSelector((state) => state.users);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUsers());
}, [dispatch]);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
<h2>User</h2>
{loading && <p>Loading</p>}
{error && <p>データ取得に失敗しました。</p>}
{users && users.map((user, index) => <div key={index}>{user.name}</div>)}
</div>
);
}
export default App;
ブラウザをリロードすると一瞬loadingが表示された後にユーザ一覧が表示されます。意図的にfetch関数で指定したURLのドメイン名から1文字変更してブラウザをリロードすると”データ取得に失敗しました”が表示されます。createAsyncThunkを利用することでただデータを非同期に取得するだけではなくLoadingやエラーを表示するといった仕組みを簡単に実装することができます。
TypeScript
Viteを利用してTypeScript環境のReactプロジェクトを作成しますが作成するコードは先ほど作成したカウンターのコードにTypeScriptの型を追加しています。
環境の構築
Redux ToolkitをTypeScriptで記述した場合のコードについて確認しておきます。ここではReactのプロジェクトはViteを利用して作成します。プロジェクト名は任意の名前のvite-redux-toolkitとします。利用するフレームワークはReact、TypeScriptを選択します。
% npm init vite@latest
Need to install the following packages:
create-vite@4.3.2
Ok to proceed? (y) y
✔ Project name: … vite-redux-toolkit
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /Users/mac/Desktop/vite-redux-toolkit...
Done. Now run:
cd vite-redux-toolkit
npm install
npm run dev
プロジェクトフォルダに移動してnpm installコマンドを実行します。
% cd vite-redux-toolkit
% npm install
Reduxとredux toolkitパッケージのインストールを行います。
% npm install @reduxjs/toolkit react-redux
Sliceファイルの作成
srcフォルダ直下にreduxフォルダを作成してcounterSlice.tsファイルを作成して以下のコードを記述します。
import { createSlice } from '@reduxjs/toolkit';
type Counter = {
count: number;
};
const initialState: Counter = {
count: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increase: (state) => {
state.count += 1;
},
decrease: (state) => {
state.count -= 1;
},
},
});
export const { increase, decrease } = counterSlice.actions;
export default counterSlice.reducer;
Storeの作成
reduxフォルダの中にstore.tsファイルを作成しcounterSliceからcounterReducerをimportしてconfigureStoreに追加します。
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
Providerの設定
main.tsxファイルでProvierでAppコンポーネントを包みます。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { store } from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
App.tsxファイルの更新
App.tsxファイルの中でカウンターの設定を行います。
import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux';
import { decrease, increase } from './redux/counterSlice';
import type { RootState, AppDispatch } from './redux/store';
import './App.css';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppDispatch = () => useDispatch<AppDispatch>();
function App() {
const count = useAppSelector((state) => state.counter.count);
const dispatch = useAppDispatch();
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increase())}>Up</button>
<button onClick={() => dispatch(decrease())}>Down</button>
</div>
);
}
export default App;
npm run devコマンドを起動して開発サーバを起動します。ブラウザでアクセスするとカウンターが表示されるのでUpボタンをクリックすると1増え、Downボタンをクリックすると1減ります。
Redux ToolkitでのTypeScriptを利用した記述方法が確認できました。