5
7

More than 1 year has passed since last update.

今更だけどRedux-Toolkit(RTK)に移行する

Last updated at Posted at 2022-04-26

react_redux.png

前書き

移行やリプレースなど、何かのきっかけがなければやりたくないですよね、特に性能面のアップが見込めなければ尚更です:sweat_smile:
Redux-Toolkitのことに関しては、前から噂は聞いてましたが、「定型文」の削減以外特にメリットなさそうだから:rolling_eyes:ずっと放置してました:upside_down:
此間Reduxの公式ドキュメントを開いてみたら、なんと!Redux-Toolkitがreduxのデフォルトスタイルになってるようです:scream:
流石にReactユーザーとしては、無視できないレベルに来たようです:sweat_smile:

Redux-Toolkitとは何か

Reduxのリポジトリにある公式なライブラリです。
Redux自体は軽量で限られた部分を担うライブラリのため、関連ライブラリなども豊富なで多くの選択肢やプラクティスが存在します。
なのでそれらを公式がまとめ、最適化したものがこのredux-toolkitです

:point_up_tone1: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

:point_up_tone1:ファイル構成は人それぞれだと思いますが、修正箇所だけ見てください。
:raised_hand_tone1:修正なしの部分は...として、記述から省きます。
インストール完了後、ますはstoreから変えていきます。

  • 既存store
index.ts
...
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,
   )),
);
...
  • 修正後
index.ts
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,
});

:point_up_tone1:redux-devtools-extensionがデフォルトで設定されており、booleanで切り替え可能です。

次はreducersを修正します。

  • 修正前
reducers.ts
...
import { combineReducers } from "redux";
...
  • 修正後
reducers.ts
...
import { combineReducers } from "@reduxjs/toolkit";
...

以上、終わりです。:point_up_tone1:
Redux-ToolkitReduxとの互換性が優れているため、これだけの修正で移行は終了です。

sliceの使用

Redux-Toolkit使用する場合、一番のメリットはslice機能です。
公式ドキュメント

sliceを作ることで、ActionReducerSelectorというそこそこの量の「定型文」を無くせます。

:point_up_tone1:具体例を見てみます
既存のスタイルでとあるリソースをredux使用して管理する場合、大体下記のような構造が必要です。

|-- store
|-- resource
|-- |-- actions.ts
|-- |-- reducer.ts
|-- |-- sagas.ts
|-- |-- index.ts
|-- index.ts
|-- reducers.ts
|-- sagas.ts

actions.tsreducer.tsに通常大量な 「定型文」が存在します:frowning2:

sliceを使用する場合:point_down_tone1:これだけで終わりです。

|-- store
|-- resource
|-- |-- slice.ts
|-- index.ts
|-- reducers.ts
|-- sagas.ts

sliceの中身は以下のように定義します。
:point_up_tone1:もしESlint使用する場合、.eslintrc.ymlに下記のルールを追加してあげる必要があります。

.eslintrc.yml
rules:
  no-param-reassign:
    - off  
slice.ts
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

:point_up_tone1:slice一個定義すれば、 actionreducerは自動的に内包されます。

非同期Reduxの処理

slice.ts
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.tssliceを追加します。

reducers.ts
...
import { counterSlice } from "./resource/slice";

export interface RootState {
  counterSlice: ReturnType<typeof counterSlice.reducer>;
}

...
export const createRootReducer = (history: History) => combineReducers<RootState>({
  counterSlice: counterSlice.reducer,
});

コンポネートでの呼び出し方。

index.tsx
...
import { getNumber } from 'store/resource/slice';

export const ResourceContainer = () => {
  const { value } = useSelector((state: RootState) => state.counterSlice);
  useEffect(() => {
    dispatch(getNumber());
  }, []);
...
}

最後

slice使用すると、直接stateに修正してるように見えますが、実はそうではないらしいです。
それ以外は見えやすくなったので、個人的には好きです。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7