Reactアプリを機能単位で分ける

機能単位の構成と依存方向のルールを使い、Reactアプリを変更しやすく保つ方法を考えます。

Published
2022年10月19日
Read in English

components/hooks/ にファイルが積み上がり、どれがどの画面のものか誰にも言えなくなる。ファイルの種類でディレクトリを切っていると、アプリが育つほどこうなります。

一つの機能を直すために遠いディレクトリを往復し、少量の変更がレビューではコードベース全体へ散らばる。途中から参加した人は、消してよいファイルさえ判断できません。厳密なルールを増やせば解決できるようにも見えますが、ルールを学び、守り、更新し続けるコストも無視できません。

そこで、Reactアプリの設計例をまとめたBulletproof Reactを参考に、機能を中心とした構成を試しました。ディレクトリの名前より、何を解決するのか、境界をどこに置くのか、チームが同じ判断を繰り返せるのかを考えます。

GitHub - alan2207/bulletproof-react: 🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications.🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications. - alan2207/bulletproof-reactgithub.com

アーキテクチャに求めること

目指したのは、次の2点です。

  • プロジェクトへの参加時期やReactの経験にかかわらず、似た要件は似た構成になる
  • 1つの機能を変更するために必要なファイルが、できるだけ近くに集まる

以前は、ファイルの種類ごとに次のようなディレクトリへ分けていました。

src/
├── components/
├── hooks/
├── models/
├── modules/
└── pages/

小規模なうちは理解しやすいものの、アプリが大きくなると componentshooks が肥大化します。ページ専用の部品を pages/components に置く方法もありますが、「将来共有するかもしれない」という予測が配置の判断に入り込みます。

作成時点で再利用性を見極めることは困難です。判断を個人へ委ねると、不要な共通部品が増えたり、同じ機能の実装が複数の場所へ散らばったりします。

機能を中心にファイルを集める

ファイルの種類ではなく、どの機能を実現するかを配置の基準にします。お気に入り機能なら、UI、API、Hook、型、テストを features/favorites の近くへまとめます。

src/
├── app/
├── components/
├── features/
│   ├── favorites/
│   └── products/
└── pages/

components には、Buttonのように機能へ依存しない共有部品だけを置きます。共有するか迷うものは、まず利用する機能の内側へ置き、実際に複数の機能から必要になった時点で移動します。

この構成では、アプリケーションの要件が配置の判断材料になります。機能を削除するときも、関係するコードの範囲を把握しやすくなります。

機能間の依存を制限する

商品表示からお気に入り登録を呼び出すように、機能同士が連携すると依存が生まれます。機能の内部ファイルをどこからでも直接importできると、変更の影響がコードベース全体へ広がります。

そこで、各機能が外部へ公開するものを index.ts などの公開APIに限定します。

// features/favorites/index.ts
export { FavoriteButton } from "./components/favorite-button";
export type { Favorite } from "./types";

利用側は公開APIからだけimportします。

// NG: 機能の内部構造へ依存する
import { favoriteActions } from "@/features/favorites/modules/actions";

// OK: 公開APIへ依存する
import { FavoriteButton } from "@/features/favorites";

このルールはESLintの no-restricted-imports や、依存関係を検査するツールで補助できます。ただし、Linterだけであらゆる相対パスやエイリアスを完全に制御しようとすると、設定そのものが複雑になります。

自動化できる重要な違反はLinterで防ぎ、それでも表現しにくい原則はレビューで確認します。どこまでを機械に任せるかも、保守コストを含めて決めます。

機能の境界をどう決めるか

最も難しいのは、何を1つの機能と呼ぶかです。私は、ユーザーが認識する対象と操作を手がかりにしています。

まず商品、お気に入り、注文といった対象を挙げ、次にそれぞれ何ができるかを整理します。追加する、削除する、一覧を見る。そうした操作が、境界を考える材料になります。

機能のモデリング例

これは必ずしもオブジェクト指向のクラス設計へ落とし込むという意味ではありません。プロダクトの言葉で機能を分けることで、デザイン、実装、ディレクトリ構成の間に共通の語彙を作ることが目的です。

境界に迷ううちは、細かく分けすぎません。いつも一緒に変わる機能は、同じ場所にある方が自然です。片方を直すたびに無関係なコードまで揺れるようになったら、そこが分離を考える合図です。

ガイドをそのままテンプレートにしない

Bulletproof React自身も、テンプレートやフレームワークではなく、考え方を示すガイドだと明記しています。ディレクトリ構成をコピーすることより、プロジェクトとチームに合う原則を選び、一貫して運用することが重要です。

私はAPI処理を apihooks のどちらへ置くか、ルート単位のコンポーネントをどこへ置くかなどを、利用するフレームワークとチームの理解しやすさに合わせて決めます。default export の可否や命名規則も、実際に判断のばらつきを減らせるならルールにします。

さいごに

よいディレクトリ構成は、変更に必要なコードを見つけやすくし、依存の向きを理解でき、チームが同じ基準で次のファイルを置ける構成です。見た目の整い方は基準になりません。

機能単位でコードを近づけ、公開APIで境界を作り、必要以上に共通化しない。Bulletproof Reactは、その議論を始めるための優れた土台になりました。

参考文献