前書き
移行やリプレースなど、何かのきっかけがなければやりたくないですよね、特に性能面のアップが見込めなければ尚更です
Redux-Toolkit
のことに関しては、前から噂は聞いてましたが、「定型文」の削減以外特にメリットなさそうだからずっと放置してました
此間Reduxの公式ドキュメントを開いてみたら、なんと!、Redux-Toolkit
がreduxのデフォルトスタイルになってるようです
流石にReactユーザーとしては、無視できないレベルに来たようです
Redux-Toolkitとは何か
Reduxのリポジトリにある公式なライブラリです。
Redux自体は軽量で限られた部分を担うライブラリのため、関連ライブラリなども豊富なで多くの選択肢やプラクティスが存在します。
なのでそれらを公式がまとめ、最適化したものがこのredux-toolkitです
Reduxの最適化ツールの訳です。
公式ドキュメント
既存プロジェクト構成
- react: 17.0.1
- redux: 4.0.5
- react-redux: 7.2.2
- redux-saga: 1.1.3
移行手順
Redux-Toolkit
をインストールします。
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
ファイル構成は人それぞれだと思いますが、修正箇所だけ見てください。
修正なしの部分は...
として、記述から省きます。
インストール完了後、ますはstore
から変えていきます。
- 既存store
...
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import { createRootReducer } from "./reducers";
import { composeWithDevTools } from "redux-devtools-extension";
const history = createBrowserHistory();
const reactRouterMiddleware = routerMiddleware(history);
const sagaMiddleware = createSagaMiddleware();
...
const store = createStore(
createRootReducer(history),
composeWithDevTools(applyMiddleware(
reactRouterMiddleware,
sagaMiddleware,
)),
);
...
- 修正後
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import { createRootReducer } from "./reducers";
...
const history = createBrowserHistory();
const reactRouterMiddleware = routerMiddleware(history);
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: createRootReducer(history),
middleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), sagaMiddleware, reactRouterMiddleware],
devTools: true,
});
redux-devtools-extension
がデフォルトで設定されており、booleanで切り替え可能です。
次はreducers
を修正します。
- 修正前
...
import { combineReducers } from "redux";
...
- 修正後
...
import { combineReducers } from "@reduxjs/toolkit";
...
以上、終わりです。
Redux-Toolkit
はRedux
との互換性が優れているため、これだけの修正で移行は終了です。
sliceの使用
Redux-Toolkit
使用する場合、一番のメリットはslice
機能です。
公式ドキュメント
slice
を作ることで、Action
、Reducer
、Selector
というそこそこの量の「定型文」を無くせます。
具体例を見てみます
既存のスタイルでとあるリソースをredux使用して管理する場合、大体下記のような構造が必要です。
|-- store
|-- resource
|-- |-- actions.ts
|-- |-- reducer.ts
|-- |-- sagas.ts
|-- |-- index.ts
|-- index.ts
|-- reducers.ts
|-- sagas.ts
actions.ts
や reducer.ts
に通常大量な 「定型文」が存在します
slice
を使用する場合これだけで終わりです。
|-- store
|-- resource
|-- |-- slice.ts
|-- index.ts
|-- reducers.ts
|-- sagas.ts
slice
の中身は以下のように定義します。
もしESlint使用する場合、.eslintrc.yml
に下記のルールを追加してあげる必要があります。
rules:
no-param-reassign:
- off
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } as CounterState
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
},
},
})
// 下記二行に注目してください。
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
slice
一個定義すれば、 action
とreducer
は自動的に内包されます。
非同期Reduxの処理
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } as CounterState
export const getNumber = createAsyncThunk(
"counter/getNumber",
async () => {
const { data } = await incrementAPI();
return data.number;
},
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {}
extraReducers: {
[getNumber.pending.type]: (state) => {
},
[getNumber.fulfilled.type]: (state, action) => {
state.value = action.payload;
},
[getNumber.rejected.type]: (state) => {
},
},
})
reducers.ts
にslice
を追加します。
...
import { counterSlice } from "./resource/slice";
export interface RootState {
counterSlice: ReturnType<typeof counterSlice.reducer>;
}
...
export const createRootReducer = (history: History) => combineReducers<RootState>({
counterSlice: counterSlice.reducer,
});
コンポネートでの呼び出し方。
...
import { getNumber } from 'store/resource/slice';
export const ResourceContainer = () => {
const { value } = useSelector((state: RootState) => state.counterSlice);
useEffect(() => {
dispatch(getNumber());
}, []);
...
}
最後
slice使用すると、直接stateに修正してるように見えますが、実はそうではないらしいです。
それ以外は見えやすくなったので、個人的には好きです。