Redux ToolkitとはReduxの記述を簡潔にするためのツールです。
Redux Toolkitを利用することで、以下のようなReduxの問題点が解決できます。1
- 複雑なStoreの設定方法
- 冗長なコード
- 機能を拡張する際に別途行うライブラリのインストール作業
Redux Tookitで提供されているAPIは以下の通りです。2
- configureStore()
- createReducer()
- createAction()
- createSlice()
- createAsyncThunk
- createEntityAdapter
- createSelector
今回はReact ReduxのBasic Tutorialで紹介されているサンプルアプリケーション(Todoアプリ)をRedux Toolkitで書き換えたので紹介します。
コードはTypeScriptで記述しています。React ReduxのチュートリアルをTypeScriptに書き換える手順は『React Reduxのチュートリアル(Todoアプリ)をTypeScriptで書き換えてみた』を参考にしてください。
reduxjs/toolkit
は1.5.0、react-redux
は7.1.16、react
は17.0.1、typescript
は3.8.3を利用しています。
目次
Redux Toolkitのインストール方法
create-react-app
コマンドでアプリケーションを作成する場合、redux
のインストールにRedux Toolkitが利用されます。
ですので、--template redux
もしくは--template redux-typescript
でRedux Toolkitがインストールされます。
### JavaScriptの場合
$ npx create-react-app my-app --template redux
### TypeScriptの場合
$ npx create-react-app my-app --template redux-typescript
既存のアプリケーションにRedux Toolkitのみをインストールする場合は以下のようにします。
### NPM
$ npm install @reduxjs/toolkit
### Yarn
$ yarn add @reduxjs/toolkit
StoreをconfigureStore()で書き換える
configureStore()
を利用すると非同期処理のミドルウェアであるredux-thunk
や、デバッグを行う際に利用するRedux DevTools Extensionが有効になります。
createStore()
を利用する場合、Redux DevTools Extensionを有効にするには以下のように書く必要がありました。
src/store.ts
import { createStore } from "redux";
import rootReducer from "./reducers";
export default createStore(
rootReducer,
(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
(window as any).__REDUX_DEVTOOLS_EXTENSION__()
);
configureStore()
を利用すると以下のように書き換えられます。3
src/store.ts
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({ reducer: rootReducer });
export default store;
ActionをcreateAction()で書き換える
通常のActionの実装手順は以下の通りです。
- ActionTypeの定義をする
- ActionTypeを返すActionCreatorの実装をする
Actionとはtype
とpayload
をプロパティに持つオブジェクト、ActionCreatorとはActionを返すメソッドです。
createAction()
は『引数で指定したActionTypeを返すActionCreator』を作成します。
つまり、createAction()
を利用することで上記2つの実装を1つにまとめられます。
たとえば、『Todoを追加する』というAction(addTodo
)は通常以下のように実装します。
src/redux/actions.ts
import { ActionTypes } from "./actionTypes";
let nextTodoId = 0;
type AddTodoAction = {
type: ActionTypes.ADD_TODO;
payload: {
id: number;
content: string;
};
};
export const addTodo = (content: string): AddTodoAction => ({
type: ActionTypes.ADD_TODO,
payload: {
id: ++nextTodoId,
content,
},
});
上記をcreateAction()
を利用して書き直すと以下のようになります。
src/redux/actions.ts
import { createAction } from "@reduxjs/toolkit";
let nextTodoId = 0;
export const addTodo = createAction(
ActionTypes.ADD_TODO,
(content: string) => (
{
payload: {
id: ++nextTodoId,
content,
}
}
)
)
Todoアプリをで利用するActionをcreateAction()
で書き直した全体像は以下の通りです。
src/redux/actions.ts
import { ActionTypes } from "./actionTypes";
import { VisibilityFilterTypes } from "./types";
import { createAction } from "@reduxjs/toolkit";
let nextTodoId = 0;
type AddTodoAction = {
type: ActionTypes.ADD_TODO;
payload: {
id: number;
content: string;
};
};
export const addTodo = createAction(
ActionTypes.ADD_TODO,
(content: string) => (
{
payload: {
id: ++nextTodoId,
content,
}
}
)
)
type ToggleTodoAction = {
type: ActionTypes.TOGGLE_TODO;
payload: {
id: number;
};
};
export const toggleTodo = createAction(
ActionTypes.TOGGLE_TODO,
(id: number) => (
{
payload: {
id,
},
}
)
);
type SetFilterAction = {
type: ActionTypes.SET_FILTER;
payload: {
filter: VisibilityFilterTypes;
};
};
export const setFilter = createAction(
ActionTypes.SET_FILTER,
(filter: VisibilityFilterTypes) => (
{
payload: { filter },
}
)
);
export type TodoActions = AddTodoAction | ToggleTodoAction | SetFilterAction;
ReducerをcreateReducer()で書き換える
Reduxでは以下のようにswitch文を利用してReducerを実装します。
src/reducers/todos.ts
import { ActionTypes } from "../actionTypes";
import { TodoActions } from "../actions";
import { TodoState } from "../types";
const initialState: TodoState = {
allIds: [],
byIds: {},
};
const todos = (state = initialState, action: TodoActions) => {
switch (action.type) {
case ActionTypes.ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false,
},
},
};
}
case ActionTypes.TOGGLE_TODO: {
const { id } = action.payload;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed,
},
},
};
}
default:
return state;
}
};
export default todos;
しかし、上記の記法には以下のような問題点があります。4
- dafault caseを書き忘れる可能性がある
- initial stateの設定を忘れる可能性がある
- switch文の記述が若干冗長
createReducer()
を利用することで上記の問題が解決できます。
createReducer()
にはbuilder callback
とmap object
の2つの記法があります。
builder callback
記法のほうが推奨されているため4、今回はbuilder callback
を利用します。
builder callback
で用意されているメソッドは以下の通りです。5
- addCase: 特定のActionTypeのReducerを実装する場合に利用
- addMatcher: 条件にマッチしたActionTypeのReducerを実装する場合に利用
- addDefaultCase: 上記2つにマッチしなかったActionTypeのReducerを実装する場合に利用
createReducer()
で書き直すと以下のようになります。
builder.addCase
でActionTypeと一致した時に実行するReducerを追加しています。builder.addCase
の第一引数はActionCreator、第二引数はReducer(ActionとStateを受け取り、新しいStateを返す関数)となります。
src/redux/reducers/todos.ts
import { addTodo, toggleTodo } from "../actions";
import { TodoState } from "../types";
import { createReducer } from "@reduxjs/toolkit";
const initialState: TodoState = {
allIds: [],
byIds: {},
};
const todos = createReducer(initialState, (builder) => {
builder
.addCase(addTodo, (state, action) => {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false,
},
},
};
})
.addCase(toggleTodo, (state, action) => {
const id = action.payload.id;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed,
},
},
};
});
});
export default todos;
同様に、visibilityFilterをcreateReducer()
を利用して書き換えると以下のようになります。
src/redux/reducers/visibilityFilter.ts
import { VISIBILITY_FILTERS } from "../../constants";
import { VisibilityFilterTypes } from "../types";
import { setFilter } from "../actions";
import { createReducer } from "@reduxjs/toolkit";
const initialState = VISIBILITY_FILTERS.ALL as VisibilityFilterTypes;
const visibilityFilter = createReducer(initialState, (builder) => {
builder.addCase(setFilter, (state, action) => {
return action.payload.filter;
});
});
export default visibilityFilter;
ActionとReducerをcreateSlice()で書き換える
createSlice()
を利用することでActionType、ActionCreator、Reducerをまとめて定義できます。
createSlice()
は内部でcreateAction()
とcreateReducer()
を利用しています。
つまり、createAction()
とcreateReducer()
を融合したものがcreateSlice()
になります。createSlice()
を利用する場合、createAction()
とcreateReducer()
は不要となります。
createSlice()
のパラメータは以下の通りです。6
- initialState: 初期値
- name: Sliceの名前。ActionType名の接頭辞として利用される。
- reducers: Reducerの定義箇所
todosに関するAction・ReducerをcreateSlice()
で書き直すと以下のようになります。
src/redux/todosSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import { TodoState } from "../redux/types";
let nextTodoId = 0;
const initialState: TodoState = {
allIds: [],
byIds: {},
};
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
addTodo(state, action) {
const content = action.payload;
let id = ++nextTodoId;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false,
},
},
};
},
toggleTodo(state, action) {
const id = action.payload;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed,
},
},
};
},
},
});
export const { addTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;
createSlice()
を利用した場合のReducerの結合は以下のようになります。
createSlice()
でexport default
しているReducerを利用しています。
redux/reducers/index.ts
import { combineReducers } from "redux";
import visibilityFilter from "./visibilityFilter";
- import todos from "./todos";
+ import todosReducer from "../todosSlice";
export default combineReducers({
- todos,
+ todos: todosReducer,
visibilityFilter
});
createSlice()
でexport
したAction
をコンポーネントで利用するには以下のようにします。
ActionCreatorWithPayload
の第一引数にはPayloadの型、第二引数にはActionTypeの型(string
)を指定します。
src/components/AddTodo.tsx
- import { addTodo } from "../redux/actions";
+ import { addTodo } from "../redux/todosSlice";
+ import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
type AddTodoProps = {
- addTodo: (input: string) => TodoActions;
+ addTodo: ActionCreatorWithPayload<string, string>;
};
createSlice()
で自動生成されるActionTypeは[createSliceのname]/[ActionType名]
となります。
たとえば、todos
というname
のcreateSlice()
に作成したaddTodo
のActionTypeはtodos/addTodo
となります。

同様にvisibilityFilter
に関するcreateSlice()
は以下のようになります。
src/redux/visibilityFilterSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import { VISIBILITY_FILTERS } from "../constants";
import { VisibilityFilterTypes } from "../redux/types";
const initialState: VisibilityFilterTypes = VISIBILITY_FILTERS.ALL;
const visibilityFilterSlice = createSlice({
name: "visibilityFilter",
initialState,
reducers: {
setFilter(state, action) {
return action.payload;
},
},
});
export const { setFilter } = visibilityFilterSlice.actions;
export default visibilityFilterSlice.reducer;
visibilityFilterSlice
も考慮した最終的なReducerの結合は以下のようになります。
redux/reducers/index.ts
import { combineReducers } from "redux";
import todosReducer from "../todosSlice";
import visibilityFilterReducer from "../visibilityFilterSlice";
export default combineReducers({
todos: todosReducer,
visibilityFilter: visibilityFilterReducer,
});
visibilityFilterSlice
で定義されたsetFilter
をコンポーネントで利用する場合は以下のようになります。
src/components/VisibilityFilters.tsx
- import { setFilter } from "../redux/actions";
+ import { setFilter } from "../redux/visibilityFilterSlice";
+ import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
type VisibilityFiltersProps = {
activeFilter: VisibilityFilterTypes;
- setFilter: (filter: VisibilityFilterTypes) => TodoActions;
+ setFilter: ActionCreatorWithPayload<VisibilityFilterTypes, string>;
};
最終的なアプリケーション
CreateAction()とCreateReducer()を利用した実装
CreateSlice()を利用した実装
まとめ
- Redux ToolkitはReduxの記述を簡潔にするためのツール
- configureStore()でStoreを定義するとデバッグツールと非同期ライブラリが有効になる
- createAction()でActionの実装ができる
- createReducer()でReducerの実装ができる
- createSlice()でActionとReducerの実装ができる
さいごに
リファクタリングを行った最終的なコードはこちらのリポジトリで公開しております。
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!