どうも Nash です。
この記事は「Plain Redux を段階的に RTK へリファクタリングする話」の記事です。
では見ていきましょう。
仕事で不動産テックのモバイルアプリを ReactNative で開発しているのですが、Redux のロジックについてリファクタリングすることになりました。
長年、技術的負債が溜まってきている状態で特に Redux のロジック周りがつらいことになってきていて、プロジェクトの総意で時間を取ってリファクタすることになります。
ポイントとして、ゼロから実装するのではなくてすでに動いている Redux を段階的にリファクタリングをします。
リファクタリングをするにあたり Redux が提供している公式ベストプラクティスを一通り読みます。
この中で、RTK を使うことが強く推奨されています。
Use Redux Toolkit for Writing Redux Logic
RTK は Redux のベストプラクティスを詰め合わせたライブラリで、段階的なリファクタリングも行いやすいです。
というわけで、Plain な Redux から RTK へリファクタリングしていきます。
今回、リファクタリングをするプロジェクトの前提状態はこんな感じです。
これらをリファクタリングしていきます。
バージョニングしたフォルダを作っておきます。
今回、リファクタリングをする features のモジュールが 40 個以上あり、リファクタリングが完了できるかどうかわからないです。 最悪、途中でリファクタリングが止まっても、あとからフォルダを見ても状況がわかりやすくリファクタの再開もしやすい状態にしておきたいです。
そのため、バージョニングしたフォルダを作ることにしました。
features
├── v1
└── v2
ディレクトリ設計として features ベースにまとめます。
function ベースと feature ベースはこんな感じでディレクトリの区切り方です。
features
├── v1 # v1 はfunctionベース
│ ├── actions
│ │ ├── article
│ │ └── user.ts
│ ├── middlewares
│ │ ├── article.ts
│ │ └── user.ts
│ └── reducers
│ ├── article.ts
│ └── user.ts
└── v2 # v2 はfeatureベース
├── articles
│ ├── api.ts
│ ├── components
│ ├── hooks.tsx
│ ├── slice.ts
│ └── thunk.ts
└── users
├── api.ts
├── hooks.tsx
├── slice.ts
└── thunk.ts
今回リファクタするプロジェクトは、すでに feature ベースだったのでここは楽にリファクタできました。 もしも function ベースだとしたら fetures ベースになるように1つずつ根気よく v2 へ移動していきます。
また個人的な推奨として、features には Redux に関連するモジュール以外もここにすべてまとめていきます。
たとえば、hooks
、utils
、converter
、decorator
、api
などなど。
component
、constants
などもまとめていいけど、プロジェクトごとの決めの問題なのでルールを決めて README などどこかに明記するのがいいかと思います。
ベストプラクティスに従って actionName の命名規則を決めましょう。
# convention
[domain]/[eventName]
# ex
user/addItem
article/publish
現在のベストプラクティスでは RTK を使いますが、過去は ducks パターンが一般的に利用されていました。
ducks パターンでは、命名規則が大文字+ Underline (ex)USER_SAVE
なので、現在のベストプラクティスと異なるので注意です。
特に、RTK のcreateAsyncThunk
で生成される actionName が camel+スラッシュで自動で生成されるので、今のプラクティスに出来る限り沿ったほうがいいです。
// tsx
createAsyncThunk('user/updateWithStorage', async () => {...})
// action name
// => /pending, /fulfilled, /rejected が付与される
user/updateWithStorage/pending
user/updateWithStorage/fulfilled
user/updateWithStorage/rejected
ちなみに、今回のプロジェクトではこの命名規則をすこし拡張しています。 コンテキストとバージョンも ActionName に含めています。
- context:1つのアプリに2つのコンテキストが別れてる
- version:リファクタ前後がわかるようなバージョン
# convention
[context]/[version]/[domain]/[eventName]
# ex
rent/v2/users/saveWithStorage
プロジェクトによってこのようにある程度は拡張するのがいいかと思います。
また、actionNameCreater な helper 関数を作っておくと便利なので用意しておきます。ちなみに型はきちんとリテラル型になります。
//
// utils.ts
//
const createRentV2ActionName = <T extends string>(text: T) =>
`rent/v2/${text}` as const;
//
// user/thunk.ts
//
import {createRentV2ActionName as actionName} from '~/src/utils';
const prefix = <T extends string>(eventName: T) =>
actionName(`user/${eventName}`);
const saveWithStorageThunk =
createAsyncThunk(prefix('saveWithStorage'), async () => {...})
// => actionName = rent/v2/user/saveWithStorage
const readFromStorageThunk =
createAsyncThunk(prefix('readWithStorage'), async () => {...})
// => actionName = rent/v2/user/readWithStorage
ちなみに、これに合わせて Debugger も見やすいようにカスタマイズしたのを作ったので、チームでもこれを使うようにしています。
いよいよリファクタリングしていきます。
先に middleware をリファクタリングをするほうが進めやすいケースが多かったので、個人的にはこっちからするのをおすすめでした。
こんな感じで thunk を書きます。
//
// features/user/thunk.ts
//
const saveWithStorageThunk = createAsyncThunk(
'article/saveWithStorage',
async (articleId, {dispatch}) => {
await saveToStorage(articleId);
dispatch(save({articleId}));
}
);
// components
dispatch(saveWithStorageThunk());
これで plain mmiddleware を RTK 経由で thunk 化しました。
同じような感じで、slice を書きます。こんな感じですね。
//
// features/user/slice.ts
//
const slice = createSlice({
name: 'article',
initialState,
reducers: {
save: (state, {payload}) => {
state.article.id = payload.articleId;
},
},
});
export default slice.reducer;
export const {save} = slice.actions;
これで slice もリファクタ完了です。
ちなみに、API についても理想としてはリファクタリングしたいですが基本的に後回しにしてます。
RTK Query の詳細はこちらをどうぞ
Plain な Redux を RTK へリプレイスしたときの記事でした。
この記事を書いてる時点では、40 個のうち半分も終わってないですが地道に終わらせていきます。
年末までに終わればいいな。ではでは。