Redux Toolkit の AsyncThunk をさわってみる

Redux Toolkit をさわってみる のつづき。createAsyncThunk とは

  • Redux Toolkit の機能の 1 つで、Redux Toolkit v1.3.0 から使える
  • 非同期処理に対応した ActionCreator を生成する関数

createAsyncThunk を使用しない場合との違い

createAsyncThunk を使用しない

  • こちらでサンプルを紹介
  • createAsyncThunk を使用しない場合は、非同期処理が完了した後にdispatch(sliceに定義されたaction)のようにしてdispatch()する構成になるため、storeに依存した実装となる
export const loadTasks = (): AppThunk => {
  return async (dispatch, getState): Promise<void> => {
    const tasks = await todoApi.getTasks();

    // store(todoSlice)に依存する実装
    dispatch(todoSlice.actions.setTasks(tasks));
  };
};

createAsyncThunk を使用する

  • createAsyncThunk では、第一引数にActionType、第二引数にPayloadを生成する関数を指定するのみなので(payload を返すのみで dispatch はしない)、storeには依存しない構成となる
export const loadTasks = createAsyncThunk(
  "todo/loadTasks",
  async (_args, _thunkApi) => {
    const tasks = await todoRepo.fetchTasks();
    return { tasks };
  }
);
  • store(slice)の定義側から、上記関数を参照しextraReducersで設定する(extraReducersの書き方はなんパターンかあるけど ts の場合は以下のように書く)
export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: { ... },
  extraReducers: (builder) => {
    builder.addCase(loadTasks.fulfilled, (state, action) => {
      state.tasks = action.payload.tasks;
    });
  },
});
  • component からの呼び出し方法は変わらず、非同期処理の関数をdispatchする
  • ts の場合は以下のようにAppDispatchを型注釈として指定しないとコンパイルエラーになる
const dispatch: AppDispatch = useDispatch();
dispatch(loadTasks());
  • AppDispatchは store を定義したモジュールとかで以下のように宣言しておく
export const store = configureStore({
  reducer: combineReducers({
    todo: todoSlice.reducer,
  }),
});
export type AppDispatch = typeof store.dispatch;
  • createAsyncThunk を使用する場合は依存関係が逆転するので、モジュールに切り出して管理する場合は、これに合わせたディレクトリ構成や循環参照に配慮した構成にする(公式では 1 ファイルにまとめちゃうことを推奨してる感あり)
  • こんな風にまとめてる人もいる

createAsyncThunk を使うメリット

  • 前述の例だと依存関係が逆転して、コード量が増えたぐらいであまりメリットを感じられないけど、createAsyncThunk を使うと、非同期リクエストにおける次の各ライフサイクル毎に処理をフックすることができる
    • pending: 非同期処理中
    • fulfilled: 非同期処理の成功時
    • rejected: 非同期処理の失敗時

非同期処理の失敗時を考慮

export const loadTasks = createAsyncThunk<
  // PayloadCreatorの返却値の型
  { tasks: Task[] },
  // PayloadCreatorの第一引数の型
  void,
  // PayloadCreatorの第二引数(ThunkAPI)の型
  {
    // rejectした時の返却値の型
    rejectValue: {
      status: number;
      message: string;
    };
  }
>("todo/loadTasks", async (_args, thunkApi) => {
  try {
    const tasks = await todoRepo.fetchTasks();
    return { tasks };
  } catch (e) {
    return thunkApi.rejectWithValue({
      status: -1,
      message: "タスクデータの取得に失敗しました",
    });
  }
});
export const todoSlice = createSlice({
  ...
  extraReducers: (builder) => {
    builder.addCase(loadTasks.fulfilled, (state, action) => {
      state.tasks = action.payload.tasks;
      state.info = 'データ取得成功';
    });

    builder.addCase(loadTasks.pending, (state, action) => {
      state.info = 'データ取得中';
    });

    builder.addCase(loadTasks.rejected, (state, action) => {
      if (action.payload) {
        const title = `[${action.payload.message}] エラーの原因を調べる`;
        const newTask = makeTask(title);
        state.tasks = [newTask];
        state.info = 'データ取得失敗';
      }
    });
  }
});
  • component 側では、次のように非同期処理の成否を判定し、payload の取得ができる
const resultAction = await dispatch(loadTasks());
if (loadTasks.fulfilled.match(resultAction)) {
  // unwrapResult() で非同期処理で返却した payload が取得できる
  console.log("データ取得成功", unwrapResult(resultAction));
}
if (loadTasks.rejected.match(resultAction)) {
  console.log("データ取得失敗", unwrapResult(resultAction));
}

state の更新を既存の reducer で行う

  • createAsyncThunk による実装は、Vuex でいうところの action に似ている感じがした
  • Vuex の action の場合は非同期処理後に直接 state を更新することもできるけど、「commitしてmutationを呼び出す」ことで state の更新箇所をまとめることができ、そっちの実装が推奨されてた記憶...
  • 前述のextraReducersの実装の場合、次のように同じstateの更新が、複数箇所で重複する可能性がある
export const todoSlice = createSlice({
  ...
  reducers: {
    setTasks(state: State, action: PayloadAction<{ tasks: Task[] }>) {
      // tasksの更新
      state.tasks = action.payload.tasks;
    },
  },
  extraReducers: (builder) => {
    builder.addCase(loadTasks.fulfilled, (state, action) => {
      // tasksの更新
      state.tasks = action.payload.tasks;
    });
  },
});
  • ちなみに createAsyncThunk を使用しない場合は、reducers で定義された action を dispatch するだけなので、このような問題は起こり得ない
  • createAsyncThunk を使用する場合は、次のように書けばextraReducers側から、reducersを呼び出せる
  extraReducers: (builder) => {
    builder.addCase(loadTasks.fulfilled, (state, action) => {
      const _action = todoSlice.actions.setTasks(action.payload.tasks);
      todoSlice.caseReducers.setTasks(state, _action);
    });