[振り返り] Redux ToolkitとRedux Thunkがチーム状況にあってなかった話

Redux Toolkitがでてきて、だいぶReduxが使いやすくなったという評判ですね。

ただ、チームの状況が整っていない場合、
メンバーがReact経験が浅い場合、
必要性がなさそうな場合は、
Redux ToolkitとRedux Thunkを使ってAPIコールをするのはやめたほうが良さそうという記事です。

私のチームは、2021年上半期はRedux ToolkitとRedux Thunkにかなり苦しめられました。

## そのときの状況
- 既存サービスのリプレイスの受託案件。Next.jsを利用した大・中・小の3種類のフロントエンド開発と、サーバーサイド開発。
- 納期がかなり迫っており、担当箇所を分けて、レビューや技術共有の時間をあまり取らないという方針であった。
- プロジェクト(大)から中心に開発が進んでおり、Rudux ToolkitとRedux Thunkを使って、全データをfetchしていた。
- メンバーのアサインがうまくいっておらず、React経験が少ないメンバーのみでの開発となった。(全員SPAの開発経験はあった。)
- 納品後の保守・改修は、開発経験1~2年の後輩が担当予定だった(SPA開発経験は多くはない)

ちょっとツッコミどころがあるかもしれませんが、上記のような状況でした。

私はプロジェクトが始まってしばらくしてからアサインされ、
プロジェクト(小)を少し進めてから、プロジェクト(大)に入って一緒に進めることになりました。

アサイン当初から、APIでReduxを使う理由について議論をしたかったのですが、
議題にはしながらもその時間が十分に取れず、また修正もそれなりに発生するために、
とりあえず他の部分も、今あるものと同じように進めていこうということになりました。

## 開発を進めていってどうなったか
- APIコール部分の処理をどうしても実装できないメンバーが2人でて、タスクの処理が進まなかった。
- 私自身は進められないわけではないが、少しハマりながら進めていた。
- 状態管理系の潜在バグがかなり入り込んだ。

結果、プロジェクト(大)の進捗が芳しくないという状況になりました。
API周りで全メンバーが苦しんでいました。

その反省もあり、プロジェクト(小)の開発は、
APIコール部分にRedux Tookit, Redux Thunkを使わずに進めることにしました。
結果、APIコール部分の実装は難易度を下げることができ、
また保守・改修担当予定だったメンバーからも扱いやすいといってもらえました。

Redux Thunkを使うと、Reduxがかなり書きやすくなるという意見は結構聞いたりします。
ただ、Redux ToolkitとRedux Thunkを使うと、Axiosだけを使ったコードより複雑で、
ある程度の土台がないと苦しいです。

実際にコードをそれぞれ見てみましょう。

## Redux Toolkit, Redux Thunkを使ったAPIコールサンプル

- storeにsliceを追加
- createAsyncThunkでリクエスト情報を設定
- createSliceでpending, fulfilled, rejectedのときの挙動を設定
- apiコールのdispatch
- sliceの呼び出し
の5つの工程が必要になります。
それぞれ以下のようなコードになります。

まずはstoreにsliceを追加します。

```ts
import { combineReducers } from 'redux';
import logger from 'redux-logger';
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { sampleSlice, initialState as userSampleState } from './modules/Samples';


const rootReducer = combineReducers({
sampleState: sampleSlice.reducer,
});

const preloadedState = () => {
  return {
    sampleState: userSampleState,
  };
};

const store = () => {
  const middlewareList = [...getDefaultMiddleware(), logger];

  return configureStore({
    reducer: rootReducer,
    middleware: middlewareList,
    devTools: process.env.NODE_ENV !== 'production',
    preloadedState: preloadedState(),
  });
};

export default store;
```

次にcreateAsyncThunkでリクエスト情報を設定します。

```ts:api/Sample.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from '../Common';

export const fetchChangedExaminationTimes = createAsyncThunk(
  'examination_times/fetchChangedExaminationTimes',
  async (arg: {}, thunkAPI) => {
    const { parameters } = arg;
    try {
      const url = `samples`;
      const response = await axios.get(url, {
        params: parameters,
      });
      return response.data;
    } catch (error) {
      ~~
    }
  },
);
```

次にcreateSliceでpending, fulfilled, rejectedのときの挙動を設定します。
とりあえずこの記事ではfulfilled(正常完了)時の処理だけ書きます。

```ts:module/Sample.ts
import { createSlice } from '@reduxjs/toolkit';
import { fetchSamples } from '../api/Sample';

export const sampleSlice = createSlice({
  name: 'samples',
  initialState: initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchSamples.fulfilled, (state, action) => {
      state.samples = action.payload.samples;
      state.error = false;
    });
  },
});
```

次にapiコールのdispatchとsliceの呼び出しです。
これは同じコンポーネントに書くときも、別のコンポーネントになるときもあると思います。

```tsx:Sample.tsx
import React, { useEffect } from 'react';
import {fetchSamples} from 'Sample';

const Samples: React.FC = () => {  
  const state = useSelector(
    (state: {
      sampleState: SampleState;
    }) => state,
  );

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchSamples({}));
  }, []);
  
  return <></>;
}
```

もっと簡潔な書き方があるかもしれませんが、
新規でAPIを追加しようとすると5箇所ほど編集する必要があります。
うまく進めることができなかったメンバーもしょうがないかなと思います。

また、私自身もcreateSliceとcreateAsyncThunkの引数がわかりづらくて辛いなと感じていました。
慣れてくるとやりやすいのかもしれませんが、APIコール以外のライブラリについても調査が必要だったので、
やはりReact経験の浅いチームには辛いなと思います。

## Axiosだけを使ったAPIコールサンプル
状態管理をしていないので当たり前なのですが、
Axiosだけの方が、かなりシンプルになります。

呼び出し元と呼び出し先の2箇所に変更を加えれば、
型は別で定義が必要かもしれませんがとりあえず動きます。

```ts:Common.ts
import axios from 'axios';

const instance = axios.create(
  baseURL: 'http://example.com',
  timeout: 1000,
);

instance.interceptors.request.use((config) => {
  ~~
  return config;
});
export default instance;

```

```ts:Sample.ts
import axios from './Common';
 
export const fetchSamples = async ({parameter}): Promise<SampleInterface> => {
  try {
    const url = '/samples';
    const response = await axios.get(url, {
      params: parameter,
    });
    return response;
  } catch (error) {
    ~~
  }
};
```

```tsx:SampleComponent.tsx
import React, { useEffect } from 'react';
import { fetchSamples } from 'Sample';

const Samples: React.FC = () => {
  useEffect(() => {
    (async () => {
      fetchSamples();
    })();
  }, []);
  
  return <></>;
}
```

ベースの部分がシンプルだと、新規でAPIコール処理を作る場合も簡単です。
エラーハンドリングをしたいとか、トーストを出したいとか必要なことは追加していきました。

なお、状態管理はuseStateとuseContextのみで進めました。
useContextは再レンダリングの懸念もありますが、
開発要件と使う必要のある箇所を考えると、特に問題にはなりませんでした。

APIコール部分以外でReduxを使って書くというやり方は、もしかしたら問題なかったかもしれません。
半年前の私たちのチームにマッチしなかったのは、APIコールにReduxを使うという点です。

今後、私のチームでは、API周りでReduxを取り入れないか、
ある程度メンバーの経験値がついている場合に取り入れるか、
React Queryなど他の手段使うか比較検討しようかなと思っています。

無理のなく学習コストを見積もって、チームでできることを増やして開発を進めていきたいなと思います。