React 機能ごとにファイルをまとめて修正箇所を探しやすくする
Reactで画面を増やしていくと、コンポーネント、API通信、型、補助関数のファイルも増えていきます。
すべてをcomponentsやutilsへ入れていると、「商品編集の仕様を変えたい」と思っても、関係するファイルを探すところから始めることになります。
今回は、Reactのコードを機能ごとにまとめる構成を紹介します。フォルダ名をそろえるだけでなく、変更するときに一緒に読むコードを近くへ置くことが目的です。
商品管理と会員管理の画面があるアプリを例に、ファイルの置き場所を考えます。
種類ごとの分類だけでは探しにくくなる
最初は、次の構成でも十分です。
- src
- components
- ProductList.tsx
- ProductEdit.tsx
- MemberList.tsx
- hooks
- useProducts.ts
- useMembers.ts
- types
- product.ts
- member.ts
- components
ただし、機能が増えると、商品管理の変更だけでも複数のフォルダを移動する必要があります。
そこで、商品管理や会員管理など、機能のまとまりごとにfeatures配下へコードを置きます。APIの呼び出しや画面の部品も、その機能の近くへまとめます。
ページ・機能・共通部品を分ける
商品管理なら、たとえば次の構成にできます。
- src
- pages
- Products
- Detail.tsx
- Products
- features
- products
- api
- queries.ts
- mutations.ts
- components
- ProductDetail.tsx
- ProductEditForm.tsx
- types.ts
- api
- products
- components
- Button.tsx
- Loading.tsx
- shared
- api
- client.ts
- api
- pages
それぞれの役割は次のとおりです。
| 場所 | 担当すること |
|---|---|
pages | URLの情報を受け取り、ページとして必要な機能を組み合わせる |
features/products | 商品の取得、編集、商品固有の表示を扱う |
components | 商品や会員などの業務を知らない見た目の部品を置く |
shared | 複数の機能で使う通信などの土台を置く |
queries.tsは主にデータ取得、mutations.tsは主に更新処理をまとめるファイルです。名前そのものより、どこを探せば何があるかが一定していることが大切です。
ページではURLのIDやアクセス条件を確認し、機能側のコンポーネントを組み合わせます。ページ全体で必要なデータがあれば、ページからデータ取得用のフックを呼んでも構いません。
共通部品に業務ルールを持たせない
商品編集画面と会員編集画面の両方に保存ボタンがあるからといって、保存処理まで1つにする必要はありません。
ボタンの色や余白は共通化できても、「商品の在庫を確認する」と「会員の権限を確認する」は異なる処理です。
依存関係の目安商品編集画面 → 共通のボタン
会員編集画面 → 共通のボタン
共通のボタンは、商品APIや会員APIを呼ばないたとえば、ProductStatusBadgeが商品固有の状態を知っているなら、複数の商品画面で使っていてもfeatures/productsに置けます。利用箇所が2つになっただけで、全機能共通のフォルダへ移す必要はありません。
業務を知っている部品は機能側へ置き、汎用部品と区別すると、共通部品の変更が特定の業務ルールに引きずられにくくなります。
共通化する前に変更理由を比べる
共通化の判断では、見た目だけでなく、仕様が変わる理由を比べます。
| 候補 | 判断の例 |
|---|---|
| 読み込み中の表示 | 多くの画面で同じ役割なら共通部品にする |
| 商品の公開状態を表すラベル | 商品機能内で共有する |
| 商品フォームと会員フォーム | 入力条件が異なるので、無理に1つにしない |
| 通信失敗を共通のエラーへ変換する処理 | 機能をまたぐ通信の土台にまとめる |
共通フォームへisProduct、isMemberなどの条件が増えてきたら、まとめすぎている可能性があります。共通の入力部品だけを残し、フォーム全体は機能側に置く方法もあります。
フォルダを分けても依存関係は自動では制限されない
featuresへ置いただけでは、機能同士が自由に内部ファイルを参照することを防げません。
たとえば、商品機能が会員機能の内部フックを使い、会員機能も商品機能の内部フックを使うと、片方だけ修正しにくくなります。
複数の機能を組み合わせる必要がある場合は、ページ側で組み合わせるか、共有する情報の受け渡し方を明確にします。特定の機能の内部状態へ直接依存しない形を考えます。
フォルダ構成や開発ルールを決めても、importを自動で禁止できるわけではありません。
必要なら依存関係を検査する仕組みを追加しますが、まずはレビューで確認できる程度の、単純なルールから始められます。
既存のコードは機能単位で少しずつ移す
整理のために、すべてのファイルを一度に移動する必要はありません。
- 次に変更する機能を1つ選ぶ。
- その機能専用のコンポーネント、API処理、型を集める。
- importを修正し、型チェックと画面の動作を確認する。
- 他の機能から使われているものは、役割を確認してから移す。
移動と同時に大きな仕様変更まで行うと、不具合の原因を探しにくくなります。既存の挙動を保って整理してから、機能を変更する方が確認しやすい場合があります。
コンポーネント内部の処理を分ける方法は、以下の記事でも紹介しています。
小さなアプリでは階層を増やしすぎない
画面が1つだけなら、pages、features、componentsをすべて用意すると、かえって移動が増えることもあります。
また、ファイルが1つしかない段階で、将来を見越した空のhooksやlibを大量に作る必要もありません。
判断の目安は、「この仕様変更に関係するコードがどこにあるか」を説明しやすいかです。商品管理を直すときに商品機能のフォルダから読み始められるなら、整理の効果が出ています。