システム改修がしやすいReactのディレクトリ構成を目指して
目的
以前のディレクトリ構成では、コンポーネントの影響範囲が把握しづらくシステム改修に手間がかかる状態でした。
そこでより直感的でシステム改修に優しいディレクトリ構成へ移行することを目指しました。
これまでのディレクトリ構成
/src
├── /components # 共通の UI コンポーネント(Atomic Design)
│ ├── /atoms
│ ├── /molecules
│ ├── /organisms
│ ├── /templates
│
├── /utils # 共通のロジックや型定義
│ ├── /hooks
│ ├── /types
│ ├── /handlers
│ ├── /constants
│
├── /domain # ドメイン(ページ単位)ごとのディレクトリ
│ ├── /example1
│ │ ├── /components # ドメイン固有のコンポーネント
│ │ │ ├── /atoms
│ │ │ ├── /molecules
│ │ │ ├── /organisms
│ │ │ ├── /templates
│ │ ├── /hooks
│ │ ├── /types
│ │ ├── /handlers
│ │ ├── /constants
│ ├── /example2
│ ├── /components
│ │ ├── /atoms
│ │ ├── /molecules
│ │ ├── /organisms
│ │ ├── /templates
│ ├── /hooks
│ ├── /types
│ ├── /handlers
│ ├── /constants
│
├── /pages # Next.jsのrouter
│ ├── index.tsx
│ ├── example1.tsx
│ ├── example2.tsx
ご覧の通り、これまでのディレクトリ構成はAtomic Designベースです。
ドメインごとにコンポーネントを分け、ドメインを超えて共通化できるものは components
に配置する形を取っていました。
また、ロジックについては厳密なルールを設けず、重複が増えてきた段階で handlers
や hooks
にまとめる運用をとっていました。
問題点
Atomic Designの定義が曖昧
よくAtomic Designの問題点として挙げられますが、僕が担当しているプロジェクトでも同じ課題に直面しました。
特にmolecules
,organisms
の境界が曖昧で扱いに困っていました。
そこでmolecules
とorganisms
を統合するなどして対策は打ちましたが、それはそれで統合したディレクトリが肥大化したため、思ったような成果はでませんでした。
├── /components
│ ├── /atoms
│ ├── /structures # moleculesとorganismsを統合
│ ├── /templates
ドメインを超えたコンポーネントが多い
ドメインごとにディレクトリを構成すると、ドメインを超えたコンポーネントが大量発生します。
例えば、
「このページにもテーブルが欲しい!」
「別のページでも同じような UI が必要!」
といった状況が頻発し、あまり効果的ではありませんでした。
さらに、似たようなコンポーネントがすでに存在しているにも関わらず、新たに実装してしまうケースも少なくありませんでした。
重複に気づかないでロジックを書いてしまう
1,2によってコンポーネントは複雑化し、明確なロジックの配置場所がなかったので、ロジックの重複が発生してしまいました。
同じような機能なのになぜここだけバグが発生するんだ?みたいな状況もありました。
改善後のディレクトリ
/src
├── /components # 共通の UI コンポーネント
│ ├── /atoms
│ ├── /layout
│
├── /utils # 共通のロジックや型定義
│ ├── /hooks
│ ├── /types
│ ├── /constants
│
├── /features # 機能単位でのディレクトリ
│ ├── /table # テーブル機能に関連するディレクトリ
│ │ ├── /components # テーブル機能に関連するコンポーネント
│ │ ├── /hooks
│ │ ├── /types
│ │ ├── /constants
│ │ ├── /pages # テーブル機能を使用しているページ
│ │ │ ├── /user
│ │ │ │ └── UserTable.tsx
│ │ │ ├── /home
│ │ │ │ └── HomeTable.tsx
│ ├── /form
│ │ ├── /components
│ │ ├── /hooks
│ │ ├── /types
│ │ ├── /constants
│ │ ├── /pages # フォーム機能を使用しているページ
│ │ │ ├── /user
│ │ │ │ └── UserForm.tsx
│
├── /pages # Next.jsのrouter
│ ├── user.tsx
│ ├── home.tsx
以下の二点がポイントです
-
機能単位での切り分け(features)
-
コンテナ・プレゼンテーションパターンの採用
機能単位での切り分け(features)
目的は改修にやさしいディレクトリ構成です。
振られるタスクは基本的に機能単位であるため、例えば「テーブルに〇〇を追加して!」といった依頼が多くなります。
そのため、ディレクトリ構成も機能ベースにすることで、「どのフォルダを改修すればいいか」を明確にしました。
機能を超えるコンポーネントに関しては従来通りですが、その数は自然と最小限に抑えられると考えています。
例えば、テーブルとダイアログの2種類の機能があった場合、これらが共通で使用するコンポーネントは、最小単位であるatoms
以外、ほとんど存在しないはずです。
/src
├── /components # 今のところはatomsだけでいけてます。
│ ├── /atoms
さらに、features
配下のcomponents
ではAtomic Designを採用せず、重複するようであれば、components
に置くくらいの感じでいきます。
機能単位でディレクトリを分けることで、Atomic Designを適用するほどコンポーネントが多くなることはないですし、機能を超えたコンポーネントもほとんど存在しないため、重複にも気付きやすいと考えました。
また、こだわりのポイントとしてfeatures
配下にpages
を設置しました。
このpages
はドメインごとに区切って、その配下にAtomic Designでいうpagesを配置します。
ドメインごとに分けることで、各機能のコンポーネントを改修した場合に影響が出るページを一瞬で判別できるようにしました。
少し余分に感じる方もいらっしゃるかもしれませんが、今回は改修にやさしい設計を目指していますので、このようなアプローチをとってみました。
│ ├── /table
│ │ ├── /components # ここのcomponentsを改修したら、
│ │ ├── /pages # userとhomeページに影響がでる!
│ │ │ ├── /user
│ │ │ │ └── UserTable.tsx
│ │ │ ├── /home
│ │ │ │ └── HomeTable.tsx
│
├── /pages # Next.jsのrouter
│ ├── user.tsx
│ ├── home.tsx
コンテナ・プレゼンテーションパターンの採用
デザインパターンについてですが、ディレクトリ構成と密接に関わるため、ここで取り上げます。
これまではロジックの扱いが曖昧で、コンポーネント内に直接記述することもあれば、handlers
やhooks
に分けることもありました。
そこで、私は単純なのでUIとロジックを完全に分けるコンテナ・プレゼンテーションパターンを採用することにしました。
ただし、従来のコンテナ・プレゼンテーションパターンでは、コンテナ部分が肥大化し、props
が大量に発生する問題があります。
これを解決するために、コンテナ部分をhooks
で代用することで、props
の大量発生を防ぎました。
hooks
の肥大化問題は依然として残っていますが、testをUIとロジックに分離できるなどメリットが大きいため、現時点ではこのアプローチが有効だと考えています。
├── /features
│ ├── /table
│ │ ├── /components
│ │ ├── /hooks # ロジックを集約!
※詳しくはこちらのbookをご覧ください!
終わりに
こちらの記事で記載されている通り、機能の共通認識は必須ですね。
まだまだ発展途上だと思うので、アドバイスがあればお願いいたします!
参考
Discussion