Skip to content

Latest commit

 

History

History
225 lines (165 loc) · 13.5 KB

directory-structure.md

File metadata and controls

225 lines (165 loc) · 13.5 KB

📁ディレクトリ構成

ディレクトリ構成もまた自由度が高くプロジェクトの保守性に関係し、コンポーネントの設計と密接に関係があります。

したがって、本稿では

  • ディレクトリで利用するコンポーネント
  • 依存関係
  • それらの理由

といった視点から整理していきます。

こちらが早見表です。

ディレクトリルートに配置するコンポーネント 子ディレクトリ(components) 子ディレクトリに配置可能なコンポーネント 依存関係(参照先)
parts parts-component なし
(肥大化する場合例外的にあり)
parts-component parts
(例外:子ディレクトリ)
features model-component
view-component
あり 任意 parts
子ディレクトリ
pages model-component
view-component
あり 任意(第1子階層はmodel-component推奨) features
parts
子ディレクトリ

基本

まず初めに、各コンポーネントのベースがこちらになります。

baseComponent

コンポーネントはフォルダごとに管理され、その中に各ファイルが機能ごとに分かれています。

Component.tsx にコンポーネント設計でお伝えした構成のいずれかが入り、model-component などの場合はHooksが切り出される形です。
index.ts が用意されている理由はimportの冗長化を防ぐためです。Typescriptはフォルダ直下のindex.tsを省略し参照することができます(Barrel)。
こちらは、Component.tsxの内容をそのままindex.tsに記述したいという考え方の方もいるかもしれません。エディタの検索性向上のため本リポジトリではこちらの方式をとっていますが、プロジェクトに応じて変更してください。

以後オレンジ色のComponent図形が現れた場合、上記のファイルを包含するものとしてお読みください。

parts

parts その名の通り、グローバルに利用される parts-component を格納する場所です。

InputButton など、汎用性の高いものが配置されます。すべてのコンポーネントフォルダはpartsフォルダ直下に配置されるため、平たい構造となります。

partsコンポーネントは「汎用的な再利用性」が求められるため、依存は同一階層のpartsコンポーネントのみです。 これは

  • 状態を持たないpropsリレーであれば再利用性が損なわれないこと(再利用性が維持できること)
  • model-componentなどの状態を持つコンポーネントに依存したとたん再利用性が消えること(再利用性の消失を防ぐこと)

が理由となります。

複雑性が増すためatomic designにおけるatoms, molecules, organisms相当のディレクトリは用意していません。
コンポーネントが増加し管理しにくい際は、機能で分けた InputフォルダButtonフォルダなどを作ると良いかもしれません。

features

features 再利用される「機能」を集めるディレクトリです。bulletproof-reactの設計思想を参考にしています。
具体的な例を挙げるとプロフィールで、ヘッダーやフッター、グローバルメニューなどを含めても良いかもしれません。カウンターなどの機能も(あまり使用例が思いつきませんが) featuresディレクトリに入れて良いでしょう。

featuresディレクトリでは基本的にmodel-component を利用します。特定の機能であることから外部との接続・ロジック・状態管理が発生するためです。

プロフィールですと

  1. Hooks
    • 顔画像の取得、名前の取得
  2. JSX部分
    • 画像を受け取って表示するコンポーネント
      • (画像をアップロードするモーダルコンポーネント)
    • 名前を受け取って表示するコンポーネント

という形になりそうです。

  • 状態を持たない、再利用性がある → partsディレクトリparts-component
  • API通信や状態がある、ロジックがある、再利用性がある → featuresディレクトリmodel-componentなど

という切り分けをしましょう。

featuresディレクトリのコンポーネントは機能単位であることから肥大化が予想されます。
これは上で例に挙げたプロフィールの「クリック時の画像アップロードモーダル」に焦点を当てるとわかりやすいでしょう。

プロフィールを細分化すると

  • 画像
  • 名前
  • アイコンアップロードモーダル

などに分けられます。これを1つの .tsx ファイルにまとめてしまうと視認性が低下します。 そのためfeaturesディレクトリ には 子の componentsディレクトリ が用意されており、その機能で利用されるコンポーネントの細分化が想定されています。

  • features
    • Profile
      • components
        • Icon
        • Name
        • UploadModal

とすると視認性が上がりそうです。

記事一覧の例もわかりやすいかもしれません。

  1. features直下で ArticleListmodel-component で作る
  2. 子componentsディレクトリArticleItemparts-component で作成し、親からpropsを受け取って表示する

という構成が予想されます。

子componentsディレクトリの狙いは、

  • 子コンポーネントを切ることにより視認性を保ちつつ機能を小さな単位にまとめる
  • グローバルへのコンポーネントの露出を防ぐ

であることを抑えてください。
この考え方はこちらの記事における「限定的コンポーネント・横断的コンポーネント」の概念を掴んでいただくとより理解が深まると思います。 AtomicDesign 境界線のひき方

依存、参照先はcomponentsディレクトリかpartsディレクトリです。
同一階層の参照はプロジェクトごとに決定しましょう。基本的には同一階層は参照せず単一機能とし、pagesで組み上げる方針が良いです。

pages

pages 主にview-componentmodel-componentを用いレイアウトを行うディレクトリです。
子のcomponentsフォルダはfeaturesとよく似た性質を持ちます。

わざわざpagesディレクトリを用意している理由はこちらの思想に則っているためです。

src/pages 以下だとファイル名 = URLになるのでファイル名と付けたいコンポーネント名が必ずしも一致しなかったり、ルーティングの変更でディレクトリ階層間のファイルの移動が発生するので、そちらにComponent定義を巻き込みたくなかったためです。 こちらから発展し、それぞれのコンポーネントフォルダは平置きされるべきでしょう。ネストしてしまった場合一つのフォルダ移動が連鎖して他に影響を与えてしまうためです。参考例

また、pages/ページ名/componentsの使用戦略はSPAかJamstackサイト制作などで方針が異なることが予想されます。

SPAかつ再利用される機能が多い場合は、featuresディレクトリを積極的に利用するべきです。
一方で、SPAでもpagesごとに機能が異なる場合は pages/ページ名/components がよく利用されるでしょう。

Jamstackなwebサイト制作では、pages主体でパーツの組み上げや切り分け用途が多くなります。したがってpages/ページ名/componentsを積極利用することになりそうです。

ただ、model-component を正しく利用していれば(外部依存が正しく設計できていれば)pages子階層→featuresの移植は難しくありません。
厳密なルールのために遅くなってしまうのであれば、「共有が必要そうであればfeaturesに移す」という気持ちで実装しましょう。

補足:引数(props)を持ち内部ロジックを持つコンポーネント

🧩コンポーネント設計とディレクトリ構造📁のルールを抑えました。
これで保守性高く再利用可能な開発が可能になるはずです。

しかし、なかなかルールに収まらないコンポーネントが出現することもまた事実です…

例えばこのようなToDoリストのアイテムを例にとると

import type {Todo} from "../types/useTodo"
//  type Todo = {
//   text: string;
//   id: number;
//   checked: boolean;
// };

type Props = {
  item: Todo;
  onEdit: (id: number, text: string) => void;
  onCheck: (id: number) => void;
}

export const TodoItem = (props: Props) => {
  return (
    <li>
      <input
       type="checkbox"
       checked={props.item.checked}
       onChange={() => onCheck(props.item.id)}
      />
      <input
       type="text"
       disabled={props.checked}
       value={props.text}
       onChange={(e) => onEdit(props.item.id, e.target.value)}
      />
    </li>
  )
}
  • propsがある
  • 内部で関数がある(機能ロジックがある)

とpartsとfeatureの二つの性質がある様に見えます。
とはいえこのケースであればおそらくあまり悩みませんね。

features/ToDo/components/ToDoItem
に格納してしまえばディレクトリ問題は解決しますし、onChangeに関数が渡っているとはいえロジックと言えるほど大掛かりではありません。

では、次にドラッグ&ドロップで画像アップロードが可能なコンポーネントはどうでしょうか?

筆者はこの設計で盛大にやらかしたことがあります。
はずかしい限りなのですが、「機能があるしAPI通信もあるからfeaturesディレクトリにmodel-componentでOK!」としました。

しかしこのドラッグ&ドロップで画像アップロードが可能なコンポーネントは複数画面で利用することが後から発覚しました。

結果はというとAPI通信と密結合してしまったため再利用ができず、
API通信結果をそのまま描画しているためコンポーネントの切り分けも進まない。
リファクタリングが怖くなりコードをコピペし対処。
後から変更が入りお祈り全置換。
(今はリファクタリング済みです)

正解は、単純にimage,setImageをpropsに受け取るpartsコンポーネントにすることでした。これで依存はなくなり、どのAPI通信に対しても画像を受け取ることができます。

大切な判断基準はどの粒度で再利用するかです。 API通信ごと再利用するならfeaturesにmodel-componentを、 引数を受け取って再利用するならpartsフォルダにparts-componentを配置しましょう。
(なお、この様な肥大化への対策としてparts-componentsでも例外的にcomponentsフォルダの使用を認めています。)

参考

この項はとりわけたくさんの記事に助けられました。 どれも素敵な記事ばかりですので読んでみて下さい。




>>「🗃️状態管理」へ進む