更新日:2024/11/08 投稿日:2024/11/07
Next.jsのディレクトリ構成を、かれこれ3年くらい考えているのだが、ローカルのメモ帳に溜め込んでいても腐るだけなので一旦雑に吐き出してみる。
大きくは基本の開発ルールを定めて、チームで混乱が生じないようにしたい。
コードファイルの配置ルールを決め円滑な開発ができるようにする
ディレクトリごとにスコープを作り、ある機能で使うコードは一か所にまとめる(コロケーション)
ロジック・状態・ビューを分離し、コードの肥大化を抑制することで、見通しをよくする
大まかなルールとしてはロジックはcontroller.ts, util.tsに置き、状態はstate.tsに、ビューはview.tsxに配置する
共通部品と共通部品でないものを明確に分ける
命名規則を単純化して命名で悩んだり属人化することを防ぐ
基本的なデザインパターンはMVCをベースにしていて、controller.tsにはイベントハンドラを、state.tsには画面の状態を、view.tsxにはTSXを持つという設計。controller, state, viewの定義は以下の通り。
controller.ts
viewに差し込むイベントハンドラ群
useEffectはここに書く
state.ts
画面で利用するstateを実装する
中にはusePageState()という関数を一つだけ作り、これで画面の全状態を管理する
この中にTSXやuseEffectは書かない
view.tsx
基本的にTSXだけが書かれている
表示非表示を切り替えるために以下のようなboolean分岐はあってよいが、ネストするなどで複雑になる場合は別コンポーネントに切り出す
props.isHoge ? <Hoge /> : < />
ただ、これだとstateはModelではなくVewModelになるので、MVVCみたいな構成になってしまうので、一般的なデザインパターンから外れてしまうのがやや懸念だ。但しModelがないことで、Modelを修正したら参照している全画面に影響が出てデグレしたみたいなのは回避できると思っている。MVCというよりClean Archtectureとかレイヤーアーキテクチャの方が近いかもしれない。
└─src
├─adaptors
│ ├─HogeRequest
│ │ ├─index.ts
│ ...
├─components
│ ├─Fields
│ │ ├─Checkbox
│ │ │ ├─controller.spec.ts
│ │ │ ├─controller.ts
│ │ │ ├─view.spec.ts
│ │ │ └─view.ts
│ │ ...
│ ├─Layouts
│ │ ├─HogeLayout
│ │ │ └─index.tsx
│ │ ...
│ └─Pages
│ ├─Dashboard
│ │ ├─ui-parts
│ │ │ ├─HogeSection
│ │ │ │ ├─controller.spec.ts
│ │ │ │ ├─controller.ts
│ │ │ │ ├─view.spec.ts
│ │ │ │ └─view.ts
│ │ │ ...
│ │ ├─controller.spec.ts
│ │ ├─controller.ts
│ │ ├─state.spec.ts
│ │ ├─state.ts
│ │ ├─view.spec.ts
│ │ ├─view.ts
│ │ ├─usecase.spec.ts
│ │ └─usecase.ts
│ ...
├─hooks
│ ├─ValueState
│ │ ├─index.spec.ts
│ │ └─index.ts
│ ...
├─pages
│ ├─Dashboard
│ │ ├─index.page.ts
│ │ ├─server.spec.ts
│ │ └─server.ts
│ ...
├─resources
│ ├─RoutingConfig.ts
│ ...
└─utils
├─HttpClient
│ ├─index.spec.ts
│ └─index.ts
...
API通信などのリクエストを投げる処理だけを置く場所。投げる処理以外は一切書かない。ここではパースや例外処理を行わなず、必要な処理がある場合は、呼び出し元に委任する。
これは呼び出し元によってハンドリングが変わるケースがあり、機能AとBでは最初同じハンドリングだったが、のちに機能Aだけハンドリングが変わりアダプタ側に手を入れるというケースを防ぐためだ。要するに開放閉鎖の原則を守るためである。
Pageコンポーネント以外の全てのUIコンポーネントと、それに付随する処理を配置する場所。MVC一式がここに入る。
Modelをここに入れることに関しては悩みがあるが、MVVMのViewModelとして考える場合はそこまで違和感がないように思う。
<input type="text" />みたいなフォーム系の入力コンポーネントの部品置き場。
ここに配置されるコンポーネントは原則として状態を持たず、状態は親コンポーネントからprops経由で渡される。このため、viewとcontrollerのみがある。
ページ全体のレイアウト置き場。大外のレイアウトを入れる想定、そんなに数は生まれないと思う。
例えば以下のコードで~Layoutとなっているのがここに入る。基本的にはViewしかない想定だが、必要ならControllerなどを置いてもよい。Stateが存在することはない(props経由で差し込まれることはある)
<CommonLayout>
<FullWideViewLayout>
<ヘッダーコンポーネントとか />
</FullWideViewLayout>
<HalfWideViewLayout>
<ボディコンポーネントとか />
</HalfWideViewLayout>
<FullWideViewLayout>
<フッターコンポーネントとか />
</FullWideViewLayout>
</CommonLayout>
ページコンポーネントの実体置き場。この設計ではNext.jsのページコンポーネントはこのディレクトリにあるUsecaseを参照するための存在なので、ページ本体の実装はここに置く。
このページコンポーネント内で使う細かいUI部品置き場。src/components/Fields/**/*などにある共通UIコンポーネントや、巣のTSXの組み合わせ。stateは持たず、controllerとviewのみを持つ。
このページコンポーネントの本体。
controller
viewに差し込むイベントハンドラ群
useEffectはここに書く
state
画面で利用するstateを実装する
中にはusePageState()という関数を一つだけ作り、これで画面の全状態を管理する
この中にTSXやuseEffectは書かない
view
controllerとstateを受け取るpropsを持つ
基本的にTSXだけが書かれている
表示非表示を切り替えるために以下のようなboolean分岐はあってよいが、ネストするなどで複雑になる場合はui-parts側に実装することが望ましい
props.isHoge ? <Hoge /> : < />
usecase
getServerSidePropsの結果を受け取るpropsを持つ
controller, state, viewの橋渡しをする場所
この階層でcontrollerにstateを差し込みラップした関数を作り、viewに差し込む
```tsx
import { sendHoge } from './controller';
const ps = usePageState();
const onClickHoge = (ev: EventT) => {
// これは中でHTTPリクエストを行っており、ローディングの状態を切り替えている
sendHoge(ps.hoge, ev.target.value);
}
return ;
```
viewやcontrollerレベルで切り替わる場合は、切り替え先をcontroller, state, view, usecaseの単位で子にして、親側でラップする
共通的な状態操作用のHooks置き場。
Next.jsのページコンポーネント置き場。*.page.tsxにはusecaseコンポーネントの呼び出しのみを記述する。SSRする場合はserver.tsに関数を実装し、*.page.tsx側から参照して使う。
定数置き場。
共通処理置き場。全体的に利用する共通処理のみ配置する。局所的に使うものは置かない。
一定のドメインの範囲で利用するものをどこに置くかは考え切れていない。