React プロジェクトのディレクトリ構成
#React プロジェクトのディレクトリ設計をもう5〜6年同じようなディレクトリ構造でやっている 1個のプロジェクトではなく複数のプロジェクトで全部同じような感じ
それであまり困ったことがない
のでどんな感じにしているかをメモしていく
だいたい以下の構造で作る
code:plaintext
/src
/api
/domain
/components
/pages
/utils (任意)
index.tsx (任意)
ルーターやフレームワークは(だいたい)問わない
#Next.js だろうと React Router だろうと React Location だろうと関係ない 裏が Firestore でも #REST API でもやはり関係がない ……という程度には汎用性がある
が、唯一 #GraphQL のケースだとこのディレクトリ構成はとらないかもしれない 後述する通り、このディレクトリ構成はスキーマからの自動生成との相性がそんなに良くない
以下、それぞれの役割について
/api
ここでは API クライアントのこと
Next.js の API Routes とかではない
パラメータを受け取って、Promise<T> を返す関数をたくさん export する場所
code:typescript
// /api/users.ts
import { type User } from '../domains/User/model'
export async function getUserById(id: User'id'): Promise<User> { ... } HTTP クライアントの実装に依存させない
たとえば Promise<AxiosResponse> を返してはいけない
fetch を使ってても Promise<Response> にはしない
Firestore を叩く関数を /api に置いてもよいが、返り値にそのことを露出させない
await したら JSON の中身が来ることを期待する
したがって、ステータスコードで分岐みたいなところも(そういうのが必要になったら)↑ の関数内でやるはず
hooks をここに置かない
/api 以下で使っていいライブラリはたとえば axios や ky だけ
API を叩く hooks をこのディレクトリ以下に置かない
react-query をラップしたものとかは /components 内で良い
というか、/api 以下の関数は hooks で使うこともあれば getServerSideProps で使うこともあるはずで、そのつもりで書かれて欲しい
パラメータの組み立て関数とかは置いても良い
formdata.append() をしまくる複雑な関数、とかを /api 以下に置くのは許しても良い
/components
共通ないしまとまったコンポーネント系
中身はテストや Storybook もコロケーションする形で置く
code:plaintext
/components
/ItemList
ItemList.tsx
ItemList.test.tsx
ItemList.stories.tsx
CSS Modules ならここに .module.css とかも置かれるはず
/pages
ルーターから読まれる想定の JSX/TSX ファイルを置く
この中のディレクトリ構成はフレームワークによる
Next.js なら file-based routing のままに
App Router ならここが /app になる
React Location とかを使っていれば適当にサブディレクトリを切る
自分は #Rails の 7 actions っぽい名前をコンポーネントにつけがち code:plaintext
/pages
/items
show.tsx
index.tsx
edit.tsx
new.tsx
/domains
ロジックや型定義のたぐいはここに入れる
types.ts とか constants.ts みたいな命名にうんざりしてそれらを同じ場所に置くようにしたら楽になった
私は型定義と便利関数を両方 model.ts という名前で置くことが多い
Redux のプロジェクトの場合 actions や reducer もここに置く
code:plaintext
/domains
/Item
model.ts
/User
action.ts
model.ts
reducer.ts
reducer.test.ts
selector.ts
/domains のファイルは /domains か /utils のファイルしか import してはいけない
/domains から /api や /componentsのファイルを import したら明らかに何かが間違っている
DDD やオニオンアーキテクチャっぽさは過剰に意識しない
どうせオブジェクト指向とか DI とかしないし
循環 import を自然に避けるための方便として「ドメインの依存関係」みたいなものを持ち出してると割り切る
強いて言うなら他の言語がクラス単位でやってる依存関係の整理をファイル単位でやると考える
/utils
特に言うことがない。名前は /lib とかでも良い
逆に、決して作らないディレクトリの一覧
/hooks
少なくともトップレベルには作らない
コンポーネントに特有のものはコロケーションする
useFormState.ts をふつうに Form.tsx の隣におく
汎用的なもの( useLocalStorage とか )は utils/hooks.ts みたいなところに置く
/containers /atoms
作らない
人間に /components より細かい分類は不可能なので諦めたほうが良い
状態の多いコンポーネントに個別に 〇〇Container って名前をつけるのは別に良いと思う
/constants
意味がわからない
だいたい domains か utils 以下で良いはず
/contexts /providers
作らない
コンポーネントにコロケーションする
〇〇Provider.tsx と use〇〇 を /components 以下に置けば良い
/repositories
/api でよい
何がドメインから見て「外部」なのか、の議論をフロントエンドでやるのは諦めたほうが良い
クライアント内かサーバー側か、だけを考えれば良い
サーバーとのやり取りは api、それ以外は全部 utils で良い
「厳密に言えば LocalStorage だってリポジトリにすべきだ」みたいな話をしない
/tests /specs
トップレベルには作らない
基本的に当該ファイルの隣に .test.ts をコロケーションする
#Jest なら当該ファイルの隣に __tests__ みたいなディレクトリを切ったりするのは良い 弱点だと思っている部分
API 型定義の自動生成との相性
この記事では基本的に、/api からは /domains 以下の型を import して使うことになっている
「/domains 以下の型が正」という構造になる
逆方向の import は容易に循環参照を生むのでやらない
/domains は /api のものを import しない
しかしたとえば #OpenAPI からクライアントを自動生成していると、/api 以下の型が正になる /api に吐き出された型を /domains が import し、/domains に置かれた便利関数を /api が import …みたいなのを避けるのが難しくなる
私は OpenAPI をあきらめて、/domains 以下に zod を置いてその型が正という状況にしてしまうことがしばしばある
Firestore のスキーマをこの方法で定義したり、古い API のスキーマも zod で手書きすると簡単
features/ のような(機能ごとに名前空間を分けたい)思想と衝突することがある
/features/auth/ にコンポーネントと API クライアントを全部置くみたいなやつ
「このコンポーネントはこの GraphQL のクエリを通じてしかサーバーにアクセスしない」というのを表現する場合、/api と /components を別ディレクトリにする旨味があまりない
そういうときは /features ディレクトリ内に tsx と Query と Mutation を並べたくなるはず
この記事の設計は「/api と /components と /domains を疎にすることに旨味がある状況」でしか活きない
REST API や Firestore にはこの考えが通用する
「コンポーネントのコロケーションはするけど API は別」みたいな前提があるケースに向いてる
厳密には /api /components /domains を持ったディレクトリが1個のアプリケーションに複数あってもよい
が、機能単位というよりはもうちょっと大きい切り方のほうが向いてる
/admin とそれ以外で別( それぞれが異なる /api /components /domains を持つ )とか
Next.js だと src/api と src/pages/api がある感じになる
後者はサーバーサイドの API ハンドラの実装