Reduxの基礎として”Reac初心者でも読めば必ずわかるReactのRedux講座”を公開していますが本文書ではRedux Toolkitの使用方法について説明を行っています。

シンプルなコードで理解を深めるために最初にRedux Toolkitを利用しない場合のカウンターのコードを記述した後にRedux ToolkitをインストールしてRedux Toolkitを利用したコードに書き換えていきます。

後半にはRedux Toolkitにおける非同期処理についての説明も行なっているのでRedux ToolkitにおけるRedux Thunkの利用方法も理解することができます。

Redux Toolkitとは

Reduxは複数のコンポーネントからアクセス可能なデータを一箇所で一元管理するためのライブラリです。本文書で説明を行うRedux Toolkitの利用は必須ではなくReduxのみ利用しても同じアプリケーションを開発することは可能ですがReduxのみでは以下のような問題点を抱えていると言われています。

  • Redux Storeを構成することが複雑すぎる
  • 大規模のアプリケーションを構築するにはたくさんの追加パッケージをインストールする必要がある
  • Reduxを利用するためには定型文的なコードを大量に記述する必要がある

上記の問題点を解消することを目的に開発されたパッケージがRedux Toolkitです。Reduxの公式ホームページではReduxを利用する際にRedux Toolkitを利用することを推奨しています。

Redux Toolkitを利用することで定型分的なコード量を減らすことができる上、開発者が陥りがちな問題を避けるためにこれまで蓄積されたベストプラクティスが反映されているためReduxのコードを効率よく記述できるようになっています。

デフォルトでRedux Devtoolsの設定が行われ、非同期処理に利用されるRedux Thunkもインストールすることなしに利用できるといったこともRedux Toolkitでは行われています。

Reactプロジェクトの作成

Redux Toolkitの動作確認を行うためにnpx create-react-appコマンドでReactプロジェクトの作成を行います。


 % npx create-react-app redux-toolkit-beginner

Reduxを利用しないカウンターの作成

プロジェクトフォルダ作成後App.jsを開いてRedexを利用しない場合のカウンターのコードを記述します。

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ファイルを開いてAppコンポーネントをProviderで包みます。Providerはreact-reduxからimportします。さらに作成したstoreをimportしてstoreを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に関する設定をcounterSlice.jsファイルにすべて設定します。もしユーザ情報をRedux Toolkitで管理したい場合はuserSlice.jsといった名前の別ファイルを作成しユーザ情報の初期値や更新に利用する関数を記述します。Sliceでデータを目的別にわけることでデータ管理を混乱をさけ、効率的に管理することができます。Sliceファイルの中で初期値、reducer、Action Creatorを設定することができるためそれぞれの処理を複数のファイルに分けて記述する必要がありません。

reduxフォルダの中にcounterSlice.jsファイルを作成し、Redux ToolkitからcrateSliceをimportします。creteSliceの引数にはname, initialState, reducersプロパティを持つオブジェクトを指定します。

nameにはSliceを識別するための名前を設定します。initialStateには共有するデータ(state)の初期値を設定し、reducersの中にはstateを更新するための関数を設定します。

下記のコードではsliceの名前をcounter、countの初期値を0に設定し、reducersの中で関数increaseとdecreaseのreducerを定義しています。increaseでは関数の引数にstateが入りcountの値を1増やす処理を行います。decreaseでは現在の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 Creatorを作成します。Action Creatorを後ほど出てくるdispatchで指定するためAction Creatorを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でimportしてreducerに設定します。


import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
複数のSliceを作成した場合は作成したSliceをimportしてreducerプロパティのオブジェクトに追加することで各Sliceで設定したstateをすべてのコンポーネントからアクセスすることが可能になります。combineReducersを利用しなくてもRedux Toolkitが自動で設定を行なってくれます。

これでRedux Toolkitの設定は完了です。ここからは設定したstateをAppコンポーネントで利用する方法を確認していきます。

useSelectorの設定

useSelector Hookを利用することでcounterSliceで設定したcountの値を取得することができます。stateのドットの直後に設定しているcounterはstore.jsのreducerに設定したオブジェクトのプロパティのcounterを対応します。counterSlice.jsファイルのnameで設定した”counter”ではありません。state.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を利用するだけで完了です。

useSelectorでcountの値を取得
useSelectorでcountの値を取得

useDispatchの設定

countの値を更新するためにはAppコンポーネントでAction Creatorsを呼び出す必要があります。Action Creatorsを実行するためにはdispatchが必要となるためuseDispatchをimportします。Action CreatetorsはcounterSlice.jsファイルでexportしているのでそのexportを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タブを選択することで下記の画面が表示されます。

Redux DevToolの確認
Redux DevToolの確認

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"になっていることが確認できます。

reducerのnameを変更した場合
reducerのnameを変更した場合

非同期処理の記述方法

ここでは同じ非同期処理ですが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では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;

getUsers関数の中では引数にdispatchを持つ関数を戻していることがわかります。

ブラウザを確認するとRedux Thunkを利用しない場合と同様にユーザの一覧が表示されます。

非同期で取得したユーザ情報を表示
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;

ここまでの設定でブラウザ上にユーザ情報の一覧が表示されます。

非同期で取得したユーザ情報を表示
createAsyncThunkで取得したユーザ情報を表示

fulfilledしか利用していまでしたが例えばデータ取得中はloadingを表示したいまたエラーが発生したエラー情報を保持したいといった場合に活用することができます。


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;

usersの他に新たににloadingとerrorの変数を追加します。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を利用することでこういった仕組みを簡単に実装することができます。