#はじめに
本稿ではnpx create-react-app example-app --template redux
で作成される、redux toolkitチュートリアルアプリケーションのsourceコードに、もともとコメント解説が書いてあるのですが、それをもう少しわかりやすく書き直し、追加のコメントも入れて説明しています。
Reduxツールキットとは
ReduxツールキットはもともとReduxの次の3つの問題を対処するために作られたパッケージです。
- Reduxストアの設定が複雑。
- Reduxを利用するためにはたくさんのパッケージを追加する必要がある。
- Reduxにはコピペで済むようなコードを無駄に書く必要がたくさんある。
###Reduxツールキットが提供するAPI
ReduxToolkit公式ドキュメントより
(Reduxの理解を前提に書かれているのでReduxを知らない方は飛ばしてください。)
configureStore()
: createStoreをラップして、シンプルな構成オプションと優れたデフォルトを提供します。スライスリデューサを自動的に組み合わせたり、提供するReduxミドルウェアを追加したり、デフォルトでredux-thunkを含めたり、Redux DevTools Extensionを使用できるようにしたりすることができます。
-
createReducer()
: アクションタイプのルックアップテーブルをcase reducer関数に与えることができ、switch文を書く必要がありません。さらに、immerライブラリを自動的に使用して、state.todos[3].completed = trueのように、通常のミュータティブコードでよりシンプルなイミュータブルアップデートを書くことができます。 -
createAction()
: 与えられたアクションタイプの文字列に対して、アクション作成関数を生成します。この関数自体にtoString()が定義されているので、型定数の代わりに使うことができます。 -
createSlice()
: リデューサ関数のオブジェクト、スライス名、初期状態の値を受け取り、対応するアクション・クリエータとアクション・タイプを持つスライス・リデューサを自動的に生成する。 -
createAsyncThunk
: アクション タイプの文字列と、promiseを返す関数を受け取り、その約束に基づいて保留/充足/拒否されたアクション タイプをディスパッチするサンクを生成します。 -
createEntityAdapter
: ストア内の正規化されたデータを管理するために、再利用可能なリデューサとセレクタのセットを生成します。 - ReselectライブラリのcreateSelectorユーティリティ: 使いやすさのために再移植したものです。
#Reduxとは
ReduxとはReactアプリケーションの中でstateを見通しよくどこからでも扱えるようにするための仕組みことで、
以下の機能を使用して状態の取得、伝達、保持などを行います。
- store - アプリケーションの全てのstateを保持するオブジェクト。
- action - (削除ボタンがクリックされた)などの情報を持つオブジェクト。
- dispatch - actionをstoreに伝達する。
- reducer - storeから受け取ったActionとstateを元に新しいstateを計算してstoreに返します。
Reduxツールキットを使用したコード解説
npx create-react-app my-app --template redux
// または
npx create-react-app my-app --template redux-typescript
上記のコマンドでReactアプリケーションを作成すると、あらかじめRedux Toolkitのチュートリアルアプリが付属されていて、/src/features配下にcouterというフォルダが作られています。
Reduxツールキットを使用する場合ではstoreの中に、ログイン機能周りのstateとreducerを持つloginSlicer、フォロー機能周りのstateとreducerを持つfollowSlicer、いいね機能周りのstateとreducerを持つlikeSliceといった個別のかたまり(sliceと呼ぶ)を作ります。
そして、それぞれの機能を使用する際に必要なsliceを使用する流れになります。
####counterフォルダ内のcomponentを見ていきます。
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';
const initialState = {
value: 0, // stateの初期値
status: 'idle', // extraReducersのstatusの初期値
};
// 以下の関数はthunkと呼ばれ、非同期のロジックを実行することができます。これは
// `dispatch(incrementAsync(10))`のように通常のアクションのようにdispatchできます。
// これは、`dispatch`関数を第一引数としてthunkを呼び出します。
// 非同期コードを実行したり、他のactionをdispatchすることができます。thunkは
// 一般的には、非同期のリクエストを行うために使用されます。
export const incrementAsync = createAsyncThunk(
'counter/fetchCount', // 'スライス名/メソッド名'とすることが多い。
async (amount) => {
const response = await fetchCount(amount);
return response.data;
}
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
// reducersフィールドでは、reducerを定義し、関連するactionを生成することができます。
reducers: {
increment: (state) => {
// Redux Toolkitを使うと、Reducerの中に stateを自由に変更するロジックを書くことができます。
// ですが実際には元のstateオブジェクトを変更しません。
// ロジック内でstateの変更を検知し、その変更に基づいて全く新しい
// stateオブジェクトを生成します。
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// ユーザーがアクションを起こした際に受け取った値をpayloadに持っています。
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
// createAsyncThunkを使用する際にextraReducersで
// pending(loading時),fulfilled(成功時),rejected(失敗時),の3つのケースを指定して
// 処理を書いていきます。
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
// 以下のexpotされた各actionをcomponentで使用する際、
// `onClick={() => dispatch(increment())}`のような形で
// dispatchしてstoreに出力します。
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 以下のexportされたselectCountをcomopnentで取得する際、
// `useSelector((state: RootState) => state.counter.value)`のような形で受け取ります。
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;
storeは/src/app/store.js のファイルにあります。
Redux Toolkitを使用する場合は、configureStore
の中にstoreが内包される形になります。
import { configureStore } from '@reduxjs/toolkit';
// このアプリケーションではsliceは一つしかありませんが、複数ある場合、全てのsliceをstore.jsにインポートします。
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
// sliceは一つのreducerを持ち、全てのreducerをここでまとめます。
// 左側はreducerを識別するための名称になります。
counter: counterReducer,
},
});
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
// useSelectorはslice内のstateを参照します。
const count = useSelector(selectCount);
// useDispatchはactionをsliceに伝達します。
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
const incrementValue = Number(incrementAmount) || 0;
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
// action.typeにdecrementという情報をcounterSliceのreducerに伝達します。
onClick={() => dispatch(decrement())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
// 計算の必要のない値の一時的な保持なので、reduxを使用せずuseStateを使用している。
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
// action.typeにincrementByAmount、
// action.paylordにincrementValueという情報を持って、
// counterSliceのreducerに伝達します。
onClick={() => dispatch(incrementByAmount(incrementValue))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
</div>
</div>
);
}
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
// Providerで囲ってstoreを渡すことで、APP内のどこからでもstoreにアクセスすることができます。
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
serviceWorker.unregister();
// counterSlice.jsのincrementAsync()の中で呼び出される
// データの非同期リクエストを模倣したモック関数
export function fetchCount(amount = 1) {
return new Promise((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}