Redux Toolkitを用いたリファクタ
React, Redux, TypeScriptの組み合わせだと、どうしてもファイル数が増えて、記述があちらこちらに散らばることが多々あり、リーダビリティが下がってしまう。
Redux Toolkit (RTK) を使えば、あちらこちらに記述していた書き方を集約して書ける以外にも、redux周りの面倒なパッケージのインストールやミドルウェアの部分も包含されているので、手間が省ける。
こちらの動画にわかりやすい比較サンプルが載っていたので、少々こちらでいじった上で、従来の書き方とRedux Toolkitを用いた書き方を比べてみる。
下記では、理解のしやすさ重視で、TODOリストのReduxを、従来の書き方とRTKでの書き方で1ファイルにまとめて見比べている。
コード
redux-original.ts
import { combineReducers, createStore, applyMiddleware } from 'redux'; import { v1 as uuid } from 'uuid'; import thunk from 'redux-thunk'; import logger from 'redux-logger'; import { composeWithDevTools } from 'redux-devtools-extension'; export interface Todo { id: string; desc: string; isComplete: boolean; } export interface State { todos: Todo[]; selectedTodo: string | null; counter: number; } /** * Constants */ const CREATE_TODO = 'CREATE_TODO'; const EDIT_TODO = 'EDIT_TODO'; const TOGGLE_TODO = 'TOGGLE_TODO'; const DELETE_TODO = 'DELETE_TODO'; const SELECT_TODO = 'SELECT_TODO'; /** * Actions & Action types */ interface CreateTodoActionType { type: typeof CREATE_TODO; payload: Todo; } export const createTodoActionCreator = ({ desc, }: { desc: string; }): CreateTodoActionType => ({ type: CREATE_TODO, payload: { id: uuid(), desc, isComplete: false, }, }); interface EditTodoActionType { type: typeof EDIT_TODO; payload: { id: string; desc: string }; } export const editTodoActionCreator = ({ id, desc, }: { id: string; desc: string; }): EditTodoActionType => ({ type: EDIT_TODO, payload: { id, desc }, }); interface ToggleTodoActionType { type: typeof TOGGLE_TODO; payload: { id: string; isComplete: boolean }; } export const toggleTodoActionCreator = ({ id, isComplete, }: { id: string; isComplete: boolean; }): ToggleTodoActionType => ({ type: TOGGLE_TODO, payload: { id, isComplete }, }); interface DeleteTodoActionType { type: typeof DELETE_TODO; payload: { id: string }; } export const deleteTodoActionCreator = ({ id, }: { id: string; }): DeleteTodoActionType => ({ type: DELETE_TODO, payload: { id }, }); interface SelectTodoActionType { type: typeof SELECT_TODO; payload: { id: string }; } export const selectTodoActionCreator = ({ id, }: { id: string; }): SelectTodoActionType => ({ type: SELECT_TODO, payload: { id }, }); /** * Reducers */ const todosInitialState: Todo[] = [ { id: uuid(), desc: 'Learn React', isComplete: true, }, { id: uuid(), desc: 'Learn Redux', isComplete: true, }, { id: uuid(), desc: 'Learn Redux-ToolKit', isComplete: false, }, ]; type TodoActionTypes = | CreateTodoActionType | EditTodoActionType | ToggleTodoActionType | DeleteTodoActionType; const todosReducer = ( state: Todo[] = todosInitialState, action: TodoActionTypes ) => { switch (action.type) { case CREATE_TODO: { const { payload } = action; return [...state, payload]; } case EDIT_TODO: { const { payload } = action; return state.map((todo) => todo.id === payload.id ? { ...todo, desc: payload.desc } : todo ); } case TOGGLE_TODO: { const { payload } = action; return state.map((todo) => todo.id === payload.id ? { ...todo, isComplete: payload.isComplete } : todo ); } case DELETE_TODO: { const { payload } = action; return state.filter((todo) => todo.id !== payload.id); } default: { return state; } } }; type SelectedTodoActionTypes = SelectTodoActionType; const selectedTodoReducer = ( state: string | null = null, action: SelectedTodoActionTypes ) => { switch (action.type) { case SELECT_TODO: { const { payload } = action; return payload.id; } default: { return state; } } }; const counterReducer = (state: number = 0, action: TodoActionTypes) => { switch (action.type) { case CREATE_TODO: { return state + 1; } case EDIT_TODO: { return state + 1; } case TOGGLE_TODO: { return state + 1; } case DELETE_TODO: { return state + 1; } default: { return state; } } }; const reducers = combineReducers({ todos: todosReducer, selectedTodo: selectedTodoReducer, counter: counterReducer, }); /** * Store */ export default createStore( reducers, composeWithDevTools(applyMiddleware(thunk, logger)) );
redux-rtk.ts
import { configureStore, createSlice, getDefaultMiddleware, PayloadAction, } from '@reduxjs/toolkit'; import { v1 as uuid } from 'uuid'; import logger from 'redux-logger'; export interface Todo { id: string; desc: string; isComplete: boolean; } export interface State { todos: Todo[]; selectedTodo: string | null; counter: number; } const todosInitialState: Todo[] = [ { id: uuid(), desc: 'Learn React', isComplete: true, }, { id: uuid(), desc: 'Learn Redux', isComplete: true, }, { id: uuid(), desc: 'Learn Redux-ToolKit', isComplete: false, }, ]; const todosSlice = createSlice({ name: 'todos', initialState: todosInitialState, reducers: { create: { reducer: ( state, { payload, }: PayloadAction<{ id: string; desc: string; isComplete: boolean }> ) => { state.push(payload); }, prepare: ({ desc }: { desc: string }) => ({ payload: { id: uuid(), desc, isComplete: false, }, }), }, edit: (state, { payload }: PayloadAction<{ id: string; desc: string }>) => { const matchedTodo = state.find((todo) => todo.id === payload.id); if (typeof matchedTodo !== 'undefined') { matchedTodo.desc = payload.desc; } }, toggle: ( state, { payload }: PayloadAction<{ id: string; isComplete: boolean }> ) => { const matchedTodo = state.find((todo) => todo.id === payload.id); if (typeof matchedTodo !== 'undefined') { matchedTodo.isComplete = payload.isComplete; } }, remove: (state, { payload }: PayloadAction<{ id: string }>) => { const index = state.findIndex((todo) => todo.id === payload.id); if (index !== -1) { state.splice(index, 1); } }, }, }); const selectedTodoSlice = createSlice({ name: 'selectedTodo', initialState: '', reducers: { select: (state, { payload }: PayloadAction<{ id: string }>) => payload.id, }, }); const counterSlice = createSlice({ name: 'counter', initialState: 0, reducers: {}, extraReducers: { [todosSlice.actions.create.type]: (state) => state + 1, [todosSlice.actions.edit.type]: (state) => state + 1, [todosSlice.actions.toggle.type]: (state) => state + 1, [todosSlice.actions.remove.type]: (state) => state + 1, }, }); export const { create: createTodoActionCreator, edit: editTodoActionCreator, toggle: toggleTodoActionCreator, remove: deleteTodoActionCreator, } = todosSlice.actions; export const { select: selectTodoActionCreator } = selectedTodoSlice.actions; const reducer = { todos: todosSlice.reducer, selectedTodo: selectedTodoSlice.reducer, counter: counterSlice.reducer, }; const middleware = [...getDefaultMiddleware(), logger]; export default configureStore({ reducer, middleware, });
RTKメモ
- RTKをreactプロジェクトに導入する場合に必要な最低のパッケージは、
@reduxjs/toolkit
とreact-redux
だけでこと足りる。redux
もミドルウェアも内包しているので
(RTKのpackage.json
を見ればわかる)
- RTKでの非同期処理は
redux-thunk
を用いて行われている - RTKでは
immer
を内包しているので、mutableな形でstateの更新ができる - RTKでreducer, acitionsなどを記述する際は、大体
createSlice
一本でまとまる - RTKでは
initialState
のフィールドが必須になっているので、従来のような定義漏れが起こりにくい - RTKでstateがプリミティブ型の場合は、mutateする形ではなく、従来のreduxのかたちでimmutableに書く
- RTKでreducerに渡す際に、initialStateが上記の例のようにimpureな副作用をともなうファンクションなどを含んでいる場合、直下のreducerのメソッド内に、さらに
reducer
とprepare
メソッドを書くことで、扱うことができるようになる