Redux Toolkitをさわってみる

Redux Toolkit とは

  • Redux を使いやすく簡素なものにするために作られた公式のヘルパーライブラリ
  • Redux Style Guideでも利用を推奨している
  • TypeScript 製のためコード補完が効き TypeScript との相性がよい
  • reducer と state をまとめて書けて action creator を書く必要がない
  • immer が組み込まれてるため、状態変更ロジックを簡素にできる(immutable な状態を維持してくれる)
  • Redux Thunk が標準 middleware として組み込まれている(非同期処理を書きたい時に使用する)
  • Redux DevTools Extension 用の設定が不要

reducer と state をまとめて書ける

  • xxxSilce.ts というファイル名が推奨されている
type State = {
  tasks: Task[];
};

const initialState: State = {
  tasks: [],
};

export const todoSlice = createSlice({
  name: "todo",
  initialState,
  reducers: {
    addTask(state: State, action: PayloadAction<string>) {
      ...
    },
    doneTask(state: State, action: PayloadAction<Task>) {
      ...
    },
    deleteTask(state: State, action: PayloadAction<Task>) {
      ...
    },
  },
});
import { configureStore, combineReducers } from "@reduxjs/toolkit";

// reducerをまとめる
export const store = configureStore({
  reducer: combineReducers({
    todo: todoSlice.reducer,
    foo: fooSlice.reducer,
  }),
});

// コンポーネント側でstate参照する時に、型注釈に使用するのでexport
export type RootState = ReturnType<typeof store.getState>;
// これは普通の redux を使う場合と同じ
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

action creator を書く必要がない

  • reducer を dispatch すれば良いだけなので、action creator を書く必要がない
import { useDispatch } from "react-redux";

export const TaskItem: React.FC<Props> = ({ task }) => {
  const dispatch = useDispatch();
  return (
    <li>
      <label>...</label>
      <button onClick={() => dispatch(todoSlice.actions.deleteTask(task))}>
        削除
      </button>
    </li>
  );
};
  • ちなみに上記の todoSlice の中身は以下のようになっている
{
  name: "todo",
  reducer: (state, action) => newState,

 // 以下の actions.xxx を dispatch していることになる
  actions: {
    addTask: (payload) => ({type: "todo/addTask", payload}),
    doneTask: (payload) => ({type: "todo/doneTask", payload}),
    deleteTask: (payload) => ({type: "todo/deleteTask", payload}),
  },
  caseReducers: {
    addTask: (state, action) => newState,
    doneTask: (state, action) => newState,
    deleteTask: (state, action) => newState,
  }
}
  • つまり dispatch(todoSlice.actions.deleteTask(task)) は、内部的には{type: "todo/deleteTask", payload}というアクションを dispatch してるのと同じということ

state の参照

  • state の参照にはuseSelectorを使う
import { useSelector } from "react-redux";

export const TaskList: React.FC = () => {
  const { tasks } = useSelector((state: RootState) => state.todo);

  return (
    <div>
      {tasks.length <= 0 ? (
        <p>{"登録されたTODOはありません。"}</p>
      ) : (
        {tasks.map(task => ...}
      )}
    </div>
  );
};
  • TypedUseSelectorHookを使うとRootStateの型注釈を省略しても、補完を効かせられる
//store.ts
export const useRootSelector: TypedUseSelectorHook<RootState> = useSelector;

//foo.tsx
const { tasks } = useRootSelector((state) => state.todo); //補完が効く

immer による状態変更ロジックの簡素化

  • redux の reducer では通常、 state が immutable になるように、新しい state を生成して return する必要がある
{
  doneTask(state: State, action: PayloadAction<Task>) {
    const tasks = state.tasks.map((task) => {
      return task.id === action.payload.id ? { ...task, done: !task.done } : task;
    });
    return { ...state, tasks };
  }
},
  • 次のように、state 内で管理されているオブジェクトの中身を変更する処理を書いても、immer が immutable な状態して state を再設定してくれる
{
  doneTask(state: State, action: PayloadAction<Task>) {
    const task = state.tasks.find((t) => t.id === action.payload.id);
    if (task) {
      task.done = !task.done;
    }
  }
},
  • 以前は素の redux では、 immutable.js を用いて immutable にすることが多かったけど、Redux Toolkit では immer が内部的に使われているため、得になにもしなくても上記のような記述が可能になる
  • Immutable.js を使うメリット - Runner in the High
  • 最近では Immer という Immutable.js オルタナティブが出たらしい。パット見ではコレクション系のクラスっぽいものがなく API が非常にシンプルという印象。

Redux Thunk による非同期処理の連携

  • Redux Toolkit が推奨する非同期処理のアプローチ
  • dispatch 関数を引数で取る Thunk 関数を store の外部に 定義し、この Thunk 関数内に非同期処理を書き、その処理結果を dispatch する
export const loadTasks = (): AppThunk => {
  return async (dispatch, getState): Promise<void> => {
    const tasks = await todoApi.getTasks();
    dispatch(todoSlice.actions.setTasks(tasks));
  };
};
  • Thunk 関数は、コンポーネントから dispatch して実行する
  • 上記例のように、Thunk 関数で Promise を返す関数を書けば、dispatch の戻り値も Promise となり await することができる(以下のように state の初期化を待ってからのコンポーネントのロードが可能になる。よくあるフラグ管理が不要になりうれしい)
import { useDispatch } from "react-redux";

export const TodoLoader: React.FC = () => {
  const dispatch = useDispatch();
  const Todo = React.lazy(async () => {
    await dispatch(loadTasks());
    return import("./Todo");
  });
  return <Todo />;
};

Redux DevTools Extension とは

  • 素の redux では利用に際し、次のようなコードの記述が必要だったが、Redux Toolkit ではこれが不要
const store = createStore(
  reducer,
  compose(
    process.env.NODE_ENV === "development" && window.devToolsExtension
      ? window.devToolsExtension()
      : (f) => f
  )
);

感想

  • 素の redux と比べて...
    • TS との相性が良くてうれ しい
    • 以下が不要になったため、だいぶすっきりした実装になり、手軽に導入できるようになった印象
      • switch 文をベースとした reducer の廃止
      • Action Creator の廃止
      • mutable な state 変更を裏で immutable にしてくれる
  • Vue + Vuex と比べて...
    • React + Redux のほうができることとか責務が狭くはっきりしてるので、設計に迷いが生まれにくそう
    • Vuex は非同期処理(action)の扱いに迷う(action から state を直接変更でできたり、action から action を呼ぶとかもできる)
    • Redux Thunk は「非同期処理はストア外部でやって、最後に dispatch して state を更新する」と導線がはっきりしてるので、これに割り切った設計がしやすそう
    • Vue は TS との相性以前に、いろいろなやり方ができちゃって気になる部分も多いので、個人的には React を使っていきたい