はじめに
最近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をもっと上手く使いこなせるようになりたいです