🐳

React Reduxには今後Redux Toolkitも使うのがいいと思う

2024/12/31に公開

ReactをTypeScriptで始めることが随分楽になったと感じています。Create React Appの以下のコマンドでOKです。

$ npx create-react-app my-app --template typescript

https://create-react-app.dev/docs/adding-typescript/#installation

2020年2月の中頃からReduxのテンプレートが利用可能となり、以下のコマンドで始めることができます。

$ npx create-react-app my-app --template redux
# or 
$ npx create-react-app my-app --template redux-typescript

https://github.com/reduxjs/cra-template-redux

このReduxテンプレートにはRedux Toolkitが使われています。https://redux-toolkit.js.org/
本格的に利用していくのはこれからですが、使わない場合と比べてここが便利だ(そうだ)というあたりを紹介します。

利点

初期設定が簡単

次のmiddlewareが設定済みとなります。

  • Redux Thunk
  • immutable-state-invariant
  • serializable-state-invariant-middleware

Redux Thunkは非同期通信のためのライブラリで、他2つは開発用のmiddlewareでReduxで禁止しているstateの変更を検知してくれます。

また、Redux Devtools Extensionも有効になっています。
これらを自分で適用しようとすると少々面倒です。

createSliceでActionのコードは不要に

公式ドキュメントからの抜粋となります

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1
  }
})

const store = configureStore({
  reducer: counterSlice.reducer
})

const { actions, reducer } = counterSlice
const { increment, decrement } = actions

こんな感じでAction, Reducerを定義できます。これによりほぼActionのコードがなくなるのではと期待しています。今まではComponent, Action, Reducerに対してロジックが多少入ることが気になっていましたが、Component, Reducerでそれぞれの責務でロジックを実装できそうです :smiley:

Reducerのネストはいつも通りな感じです。

const sliceA = createSlice({
  name: "reducerA",
  //...
})
const sliceB = createSlice({
  name: "reducerB",
  //...
})

import { combineReducers } from "@reduxjs/toolkit"
const mergeReducer = combineReducers({
  reducerA: sliceA.reducer,
  reducerB: sliceB.reducer,
})

非同期通信にはRedux Thunkが採用


const createUser = createAsyncThunk(
  'users/createUser',
  async () => {
    // API処理
  }
)

createAsyncThunkでは次の3つのActionを生成してくれます。

  • pending
  • fulfilled
  • rejected

sliceではextraReducersのbuilderを利用して追加します。createAsyncThunkで定義されたActionを利用するためです。

const userSlice = createSlice({
  name: "user",
  initialState: {
    user: null,
  },
  reducers: {},
  extraReducers: builder => {
    builder.addCase(createUser.pending, (state, action) => {})
    builder.addCase(createUser.fulfilled, (state, action) => {})
    builder.addCase(createUser.rejected, (state, action) => {})
  }
})

import { useDispatch } from "react-redux"

function UserComponent() {
  const dispatch = useDispatch()

  const handleSubmit = async () => {
    const resultAction = await dispatch(createUser())
    // createAsyncThunkで生成されるActionと比較して処理を変更できる
    if (createUser.fulfilled.match(resultAction)) {
      // success
    } else {
      // error
    }
  };
  return (
    <div>
      <form></form>
      <button onClick={handleSubmit}>submit</button>
    </div>
  )
}

やっておいた方がいいこと

RootReducerの型定義

TypeScriptで利用する際に用意しておいた方がいいです。以下で定義しておきます。

import { combineReducers, configureStore } from "@reduxjs/toolkit"

export const rootReducer = combineReducers({
  moduleA: moduleAReducer,
})
export type RootReducerType = ReturnType<typeof RootReducer>

RootとなるReducerの型定義はあると便利ぐらいの気持ちです。

useDispatchのラップ

import { configureStore } from "@reduxjs/toolkit"
const store = configureStore({
  reducer: rootReducer
})
export type AppDispatch = typeof store.dispatch
import { useDispatch } from "react-redux"
function useAppDispatch(): AppDispatch {
  return useDispatch<AppDispatch>()
}

createAsyncThunkで型定義のエラーを起こさないためです。

注意点

ImmerによるReducerのstateのImmutable

以下のようにstateを直接変更するようなコードを書いても問題ありません。Redux ToolkitではImmerを利用しており、実際にstateを変更しているわけではないとのことです。

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const slice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 1;
    }
  },
});

まとめ

煩雑な設定がなくなり、ActionやReducerの実装も統一できそうです。これからReduxを始める人、そうでない人にもおすすめです :smiley:

参考

Discussion