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を追加します。
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でリクエスト情報を設定します。
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(正常完了)時の処理だけ書きます。
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の呼び出しです。
これは同じコンポーネントに書くときも、別のコンポーネントになるときもあると思います。
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箇所に変更を加えれば、
型は別で定義が必要かもしれませんがとりあえず動きます。
import axios from 'axios';
const instance = axios.create(
baseURL: 'http://example.com',
timeout: 1000,
);
instance.interceptors.request.use((config) => {
~~
return config;
});
export default instance;
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) {
~~
}
};
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など他の手段使うか比較検討しようかなと思っています。
無理のなく学習コストを見積もって、チームでできることを増やして開発を進めていきたいなと思います。