zukucode
主にWEB関連の情報を技術メモとして発信しています。

React 機能ごとにファイルをまとめて修正箇所を探しやすくする

Reactで画面を増やしていくと、コンポーネント、API通信、型、補助関数のファイルも増えていきます。

すべてをcomponentsやutilsへ入れていると、「商品編集の仕様を変えたい」と思っても、関係するファイルを探すところから始めることになります。

今回は、Reactのコードを機能ごとにまとめる構成を紹介します。フォルダ名をそろえるだけでなく、変更するときに一緒に読むコードを近くへ置くことが目的です。

商品管理と会員管理の画面があるアプリを例に、ファイルの置き場所を考えます。

種類ごとの分類だけでは探しにくくなる

最初は、次の構成でも十分です。

  • src
    • components
      • ProductList.tsx
      • ProductEdit.tsx
      • MemberList.tsx
    • hooks
      • useProducts.ts
      • useMembers.ts
    • types
      • product.ts
      • member.ts

ただし、機能が増えると、商品管理の変更だけでも複数のフォルダを移動する必要があります。

そこで、商品管理や会員管理など、機能のまとまりごとにfeatures配下へコードを置きます。APIの呼び出しや画面の部品も、その機能の近くへまとめます。

ページ・機能・共通部品を分ける

商品管理なら、たとえば次の構成にできます。

  • src
    • pages
      • Products
        • Detail.tsx
    • features
      • products
        • api
          • queries.ts
          • mutations.ts
        • components
          • ProductDetail.tsx
          • ProductEditForm.tsx
        • types.ts
    • components
      • Button.tsx
      • Loading.tsx
    • shared
      • api
        • client.ts

それぞれの役割は次のとおりです。

場所担当すること
pagesURLの情報を受け取り、ページとして必要な機能を組み合わせる
features/products商品の取得、編集、商品固有の表示を扱う
components商品や会員などの業務を知らない見た目の部品を置く
shared複数の機能で使う通信などの土台を置く

queries.tsは主にデータ取得、mutations.tsは主に更新処理をまとめるファイルです。名前そのものより、どこを探せば何があるかが一定していることが大切です。

ページではURLのIDやアクセス条件を確認し、機能側のコンポーネントを組み合わせます。ページ全体で必要なデータがあれば、ページからデータ取得用のフックを呼んでも構いません。

共通部品に業務ルールを持たせない

商品編集画面と会員編集画面の両方に保存ボタンがあるからといって、保存処理まで1つにする必要はありません。

ボタンの色や余白は共通化できても、「商品の在庫を確認する」と「会員の権限を確認する」は異なる処理です。

依存関係の目安
商品編集画面 → 共通のボタン
会員編集画面 → 共通のボタン

共通のボタンは、商品APIや会員APIを呼ばない

たとえば、ProductStatusBadgeが商品固有の状態を知っているなら、複数の商品画面で使っていてもfeatures/productsに置けます。利用箇所が2つになっただけで、全機能共通のフォルダへ移す必要はありません。

業務を知っている部品は機能側へ置き、汎用部品と区別すると、共通部品の変更が特定の業務ルールに引きずられにくくなります。

共通化する前に変更理由を比べる

共通化の判断では、見た目だけでなく、仕様が変わる理由を比べます。

候補判断の例
読み込み中の表示多くの画面で同じ役割なら共通部品にする
商品の公開状態を表すラベル商品機能内で共有する
商品フォームと会員フォーム入力条件が異なるので、無理に1つにしない
通信失敗を共通のエラーへ変換する処理機能をまたぐ通信の土台にまとめる

共通フォームへisProduct、isMemberなどの条件が増えてきたら、まとめすぎている可能性があります。共通の入力部品だけを残し、フォーム全体は機能側に置く方法もあります。

フォルダを分けても依存関係は自動では制限されない

featuresへ置いただけでは、機能同士が自由に内部ファイルを参照することを防げません。

たとえば、商品機能が会員機能の内部フックを使い、会員機能も商品機能の内部フックを使うと、片方だけ修正しにくくなります。

複数の機能を組み合わせる必要がある場合は、ページ側で組み合わせるか、共有する情報の受け渡し方を明確にします。特定の機能の内部状態へ直接依存しない形を考えます。

フォルダ構成や開発ルールを決めても、importを自動で禁止できるわけではありません。

必要なら依存関係を検査する仕組みを追加しますが、まずはレビューで確認できる程度の、単純なルールから始められます。

既存のコードは機能単位で少しずつ移す

整理のために、すべてのファイルを一度に移動する必要はありません。

  1. 次に変更する機能を1つ選ぶ。
  2. その機能専用のコンポーネント、API処理、型を集める。
  3. importを修正し、型チェックと画面の動作を確認する。
  4. 他の機能から使われているものは、役割を確認してから移す。

移動と同時に大きな仕様変更まで行うと、不具合の原因を探しにくくなります。既存の挙動を保って整理してから、機能を変更する方が確認しやすい場合があります。

コンポーネント内部の処理を分ける方法は、以下の記事でも紹介しています。

複雑な画面をカスタムフックとViewへ分離する方法

小さなアプリでは階層を増やしすぎない

画面が1つだけなら、pages、features、componentsをすべて用意すると、かえって移動が増えることもあります。

また、ファイルが1つしかない段階で、将来を見越した空のhooksやlibを大量に作る必要もありません。

判断の目安は、「この仕様変更に関係するコードがどこにあるか」を説明しやすいかです。商品管理を直すときに商品機能のフォルダから読み始められるなら、整理の効果が出ています。


関連記事

  • TypeScript i18nextの動的な翻訳キーを抽出対象に含める

    i18nextでステータスに応じた文言を表示する場合、翻訳キーを動的に切り替えることがあります。ただし、実行時に正しく表示できることと、翻訳キーの抽出ツールが使用箇所を検出できることは別です。今回は、...


  • React useStateで入力フォームを作成する

    Reactでテキストボックスに入力した値を使用するには、useStateで値を保持します。今回は名前とメールアドレスを入力し、送信ボタンを押すと入力内容を表示するフォームを作成します。この例では入力値...


  • React useStateで一覧の追加と削除を行う

    Reactで一覧を表示するときは、配列をuseStateへ保存できます。今回は簡単な買い物リストを作り、項目の追加と削除を行います。配列のpushやspliceで既存のstateを直接変更するのではな...


  • React useRefで入力欄にフォーカスを当てる

    入力フォームを開いた直後や、入力エラーが出たときに、特定の入力欄へフォーカスしたいことがあります。DOMの要素自体を参照するにはuseRefを使います。currentは初回描画中などにnullになり得...


  • React useMemoで重い計算結果を再利用する

    一覧の絞り込みや並べ替えを描画のたびに行うと、データ量や計算内容によっては画面操作が遅くなります。useMemoは、依存する値が変わらない間、以前の計算結果を再利用します。productsまたはque...


  • React useEffectで無限ループが発生するときに確認すること

    ReactのuseEffectを利用したときに無限ループが発生してしまうことがあります。特に注意したいのが、ESLintのreact-hooks/exhaustive-depsで表示された警告をUpd...


  • React useEffectの後片付けでイベント登録を解除する

    画面の幅が変わったときに表示を更新したい場合、ブラウザのresizeイベントを登録できます。登録したままにすると、コンポーネントが不要になったあとも処理が残るため、useEffectの後片付けを用意し...


  • React 関数コンポーネントにプロパティを設定する

    React+TypeScriptで作成した関数コンポーネントにプロパティを設定する方法を紹介します。以下は送信ボタンのコンポーネントを作成しています。isSubmittingというプロパティを用意して...


  • Reactで兄弟コンポーネントの状態を共有する

    検索欄に入力した文字列を、別の一覧コンポーネントでも使いたい場合があります。兄弟同士で別々にstateを持つと値がずれるため、共通の親にstateを置きます。入力欄は値を受け取り、変更を親へ通知します...


  • React useStateでselectの選択値を取得する

    Reactでプルダウンの選択値を扱うには、selectのvalueとonChangeを使用します。DOMから取得するvalueは、数値の選択肢を表示していても文字列です。選択値をそのまま識別子として扱...