🌲

システム改修がしやすいReactのディレクトリ構成を目指して

2025/02/13に公開

目的

以前のディレクトリ構成では、コンポーネントの影響範囲が把握しづらくシステム改修に手間がかかる状態でした。
そこでより直感的でシステム改修に優しいディレクトリ構成へ移行することを目指しました。

これまでのディレクトリ構成

/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 に配置する形を取っていました。
また、ロジックについては厳密なルールを設けず、重複が増えてきた段階で handlershooks にまとめる運用をとっていました。

問題点

Atomic Designの定義が曖昧

よくAtomic Designの問題点として挙げられますが、僕が担当しているプロジェクトでも同じ課題に直面しました。
特にmolecules,organismsの境界が曖昧で扱いに困っていました。
そこでmoleculesorganismsを統合するなどして対策は打ちましたが、それはそれで統合したディレクトリが肥大化したため、思ったような成果はでませんでした。

├── /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

以下の二点がポイントです

  1. 機能単位での切り分け(features)

  2. コンテナ・プレゼンテーションパターンの採用

機能単位での切り分け(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

コンテナ・プレゼンテーションパターンの採用

デザインパターンについてですが、ディレクトリ構成と密接に関わるため、ここで取り上げます。
これまではロジックの扱いが曖昧で、コンポーネント内に直接記述することもあれば、handlershooksに分けることもありました。
そこで、私は単純なのでUIとロジックを完全に分けるコンテナ・プレゼンテーションパターンを採用することにしました。
ただし、従来のコンテナ・プレゼンテーションパターンでは、コンテナ部分が肥大化し、propsが大量に発生する問題があります。
これを解決するために、コンテナ部分をhooksで代用することで、propsの大量発生を防ぎました。
hooksの肥大化問題は依然として残っていますが、testをUIとロジックに分離できるなどメリットが大きいため、現時点ではこのアプローチが有効だと考えています。

├── /features           
│   ├── /table          
│   │   ├── /components 
│   │   ├── /hooks     # ロジックを集約!

※詳しくはこちらのbookをご覧ください!
https://zenn.dev/morinokami/books/learning-patterns-1/viewer/presentational-container-pattern

終わりに

https://zenn.dev/sakito/articles/af87061a5016e6#気をつけたいポイント
こちらの記事で記載されている通り、機能の共通認識は必須ですね。

まだまだ発展途上だと思うので、アドバイスがあればお願いいたします!

参考

https://zenn.dev/manalink_dev/articles/bulletproof-react-is-best-architecture
https://zenn.dev/mybest_dev/articles/c0570e67978673
https://zenn.dev/brachio_takumi/articles/2ab9ef9fbe4159
https://zenn.dev/bizlink/articles/b5c8985af8407a
https://github.com/alan2207/bulletproof-react
https://zenn.dev/knowledgework/articles/99f8047555f700

Discussion