はじめに
最近VueやReactを何度か触って得た知見を元に、個人的に良さげなディレクトリ構造を考えたいと思います
2023/02/18 追記
多くの方に見てもらえているようなので全体的にちゃんと書き直しました!
ディレクトリ構造
Next.js 13だと仮定します
src/
├── app
│ ├── globals.css
│ ├── head.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── base
│ │ ├── atoms
│ │ └── molecules
│ ├── feature
│ │ └── hoge
│ │ ├── atoms
│ │ ├── molecules
│ │ ├── hooks
│ │ ├── hoge.constants.ts
│ │ └── hoge.types.ts
│ ├── functional
│ └── page
├── hooks
├── libs
├── pages
│ └── api
├── providers
├── stores
│ └── fuga
│ ├── atoms.ts
│ ├── operations.ts
│ ├── selectors.ts
│ └── types.ts
├── types
└── utils
基本
基本は以下の構成を参考にさせていただいています
命名規則としては、複数形を基本に、components
の子ディレクトリだけは単数形にしていますが、好みの問題だと思います
app
Next.js 13で追加されたApp Directory
によるディレクトリです。詳細は割愛しますが、layout
が使いやすいのでApp Directory
が使えるなら使いたいと思ってます
components
まずは、こちらの記事をご覧ください
BCD Designは、大規模な開発ではとても有効だと思います。しかし、小規模の開発ならここまで細かく分ける必要がないんじゃないかなと使っていて思ったので、ここではbase
とfeature
のふたつに分けています
BCD Designにおける迷いにくい粒度や「明名する」という考え方は小規模開発においてもとても有効ですので、積極的に取り入れます
base
BCD Designにおけるbase
に該当します。atoms
とmolecules
の分け方はBCD Designに準拠します
feature
BCD Designにおけるdomain
に該当します。domain
は単数形で命名します
BCD Designにおけるcase
が存在しないのは、小規模の開発だと私がcase
をうまく使いこなせなかったからです。例えば、
- Button (base)
- AddButton (case)
- UserAddButton (domain)
と分けても、AddButton
はUserAddButton
でしか使われないことが多いです。case
を再利用するのってなかなか難しいんですよね
また、case
は専属のディレクトリ化することが多かったです
例えば、add
の中にAddButton
があったとして、それ以外にaddなんちゃら
を作る機会がないとします。これが専属化です
つまり、add
ディレクトリはAddButton
を作りたいがために設けられ、そのAddButton
もほとんど再利用されないという遠回り感が生まれてしまうのです。それならばdomain
に統合してしまっていいのではないかということでこの分け方を採用しています。common
も同様の理由でdomain
に統合しています
色々なディレクトリ構造を見ている方ならお分かりだと思いますが、汎用的な部品(base
)と機能ごとに分ける(feature
)やり方はfeature
ディレクトリを採用しているディレクトリ構造と意図がほぼ一緒です
では、なぜわざわざBCD Designについて持ち出したかというと、BCD Designにおけるatoms
とmolecules
の分け方や「明名する」という考え方を取り入れたいと考えたからです
atomsとmolecules
Button
などの単一的なコンポーネントはatoms
、Card
やList
などの複合的なコンポーネントはmolecules
に入れます
atoms
とmolecules
では1ファイルにpresenterとcontainerの両方を定義します。
export type HogeButtonPresenterProps = {
children: React.ReactNode
}
export const HogeButtonPresenter = ({children}: HogeButtonPresenterProps) => {
return <button>...</button>
}
export type HogeButtonProps = {
...
}
export const HogeButton = (...) => {
return <HogeButtonPresenter>...</HogeButtonPresenter>
}
presenterは基本的にそのファイル内ぐらいでしか使わないので接尾辞としてPresenterをつけます。しかし、containerは他のところでも頻繁に使われる可能性があり、毎回Containerをつけるのは面倒かつ見栄えが悪いので接尾辞はつけません(慣例的に接尾辞なし===Containerとする)
hooks
hooks
にはそのドメインのスコープでしか使用しないhookを入れます。
その他
hoge.constants.ts
にはそのドメインの定数を、hoge.types.ts
にはそのドメインのスキーマと型定義を入れます。わざわざディレクトリを切る必要はないと思います
functional
詳細は以下の記事をご覧ください
functional
はなくてもいいと思います。とりあえず私はRecoilのRecoilObserver.tsx
を置いています
page
feature
は機能を軸に分類しますが、page
は画面を軸に分類します。page
で行うのはfeature
を組み合わせることだけです。src/app/page.tsx
では直接feature
をimportせず、src/components/page
を介します。ただし、src/app/layout.tsx
だけは直接feature
をimportするのを許可します
pages
Next.jsのAPI Routes
のためのディレクトリです。API Routes
を使わないなら必要ありません
hooks
特定の機能にとらわれない汎用的なhookを入れます
providers
特定の機能にとらわれない汎用的なproviderを入れます。
そもそもproviderはあまり使わないようにします。理由は、stores
とhooks
があるのにわざわざproviders
まで必要になる場面ってなかなかないかなと思ったからです
types
特定の機能にとらわれない汎用的なスキーマや型定義を入れます。
types
の中でさらにどのように分けるかは、まだ洗練しきれていないです。正直types/index.ts
一本でいいような気もしてます。限定的な型はfeature/hoge/hoge.types.ts
で管理してますし、types
の中でさらに細かく分けるとどの程度の粒度で分ければいいかという問題が生じてしまいます
stores
Recoilなどの状態管理を伴うhookはここに入れます。詳細は以下の記事をご覧ください
が、最近Jotaiもなかなか良いなと感じているので、ここの中身は使用している状態管理ライブラリによると思います
utils
汎用的な関数などを入れます
libs
ライブラリの設定などを入れます
その他気をつけること
TailwindCSS
ReactでTailwindCSSを使う際は以下の記事の内容を参考にします
Storybookやテストファイル
hoge.stories.tsx
やhoge.spec.tsx
は対象コンポーネント(hoge.tsx
)と同階層に置きます。わざわざstories
ディレクトリや__tests__
ディレクトリを設けないということですね
おわりに
こういうの考えてもどうせ数ヶ月後には変わってるんですよね
BCD Designをもっと上手く使いこなせるようになりたいです