Reactで書くWebフロントエンドアプリケーションのディレクトリ設計について考えていること
アクティブに開発するWebフロントエンドアプリケーションという前提をゆるく置いて書く。
合わせて読みたい
これを見て思い立ったので書き方が影響を受けているし、共通する部分もある
読むにあたって前提にしてほしいこと
同じアプリケーションを同じチームがメンテしながら機能開発することを想定している
コードをよく知っている前提があるから、カジュアルに構成を大きく変えたりする
苦しいなら苦しくなくなるように部分的に構造を変えてみたりしたい
ここに書かないような細かいポリシーは、これをやっても全体が破綻しないように決める
どういう状態を目指すか
アプリケーションを書くときに考えることがそのまま表現できるように書くのが良い
綺麗に関心を表現できているなら関連するものは近くにあるはず
import X from "./X" と相対パスで書いたときに親方向への参照は起こらない
遠いものは自然に遠くにある
import X from "../../../features/X" などと書け、srcから過剰に掘り込むことがない
コンポーネントなら import X from "../../../components/X/variant" あたり
高々3段くらいでおさまる
それ以上の分類が常に必要なのは、その道具が綺麗にできていないか、もともと複雑なもの
もしかして:一級市民(後述)
src/ を ~/ と書けるようにする、くらいは検討することがある
いきなりこうやって遠くから取り出したいものって何?
ユーティリティー関数、hooks
機能を持たない外観・レイアウト用のコンポーネント
共通機能の呼び出しや、機能を持っていて広く使われるコンポーネント
そのアプリケーションにおける一級市民になっているもの
ざっくり分類すると
単純な単一関心ならマジでどうでもいいから好きにしろ、ディレクトリ構成なんかに時間を使うな
3000行ないレベルなら考える必要がないので上から書け。他人がメンテする前提があるなら多少良い感じにしろ
ただし、後述するやらないことだけ守る
複数の関心があるとき、関心間の関係に注目して考える
共通すること
src/index.tsx が手狭になってきたら App.tsx や Router.tsx が生まれる。
feature という言葉を page と近い意味で使う手癖がある。 auth みたいなものは concern function みたいな言葉遣いになることが多い
名前は扱う人たちがしっくりくるものを選ぶ
ルーティングが src/features/foo に閉じるならそこに src/features/foo/Router.tsx が生まれるかもしれない
逆に閉じなくて、かつルートのルーターでは手に負えないなら、 src/pages で各featureを組み立ててやる
特定の関心の中で他の関心と違う作りをすることを妨げない。 src/features/foo と src/features/bar が違う作りになっていても構わない
複数の関心の独立性がたかいとき
code:plaintext
/src
/index.tsx (entrypoint)
/features
/foo
/index.tsx
/components
/domains
/utils
/bar/...
/shared
/header/...
サービスの機能的には独立性が高いが全体に横断で使われるものが一級市民レベルの存在感がある場合
ex. 全体共通の機能がどこからでもモーダルダイアログで呼び出せて多様なパターンがあるページ→ src/dialogs
UI的な一級市民ではなくても同じようにトップレベルに置きうる
code:plaintext
/src
/index.tsx (entrypoint)
/features/...
/dialogs
/foo
/index.tsx
/components
/bar/...
/shared
/components
/utils
Web API
REST APIなら src/api なりにAPIクライアント実装を置きそう。自動生成してもここにざっくり置く
GraphQLなりでコロケーション重視なら src/features なり他の一級市民なりに一緒にファイルを置いたり、適当な単位のルートに一緒に書いたりする
GraphQLの処理をする部分だけ src/api に書かれそう。
やらない命名
だいたいこういうことをいってる
ライブラリ特有の概念でsrc直下にディレクトリ名をつけることはない
分類は控えめに
同じ尺度での分類が2回行われるのは危険信号
Atomic Designもこれに当たるのでやらない。結合度で多段分類してる
やりたくなってるときは分類の尺度選びを誤っている
/hooks
コンポーネントに一対一ならコロケーションするので作らない
汎用的なものは src/utils/useFooBar.ts なり
hooksが多すぎるなら src/utils/hooks/useFooBar.ts は考えてもいいが最初からは作らない
/containers
古いReact Reduxの慣習を引きずっているなら残っているかもだが、新規にはいらない
Container Component / Presentational Componentをやりたいなら components/Xxx/XxxContainer.tsx とする
/components/atoms などのAtomic Designやこれに類する概念の道具
やめておくのが無難
「 共通で使うつもりの components 配下はそうなっててもいいかも」と言うことはあるが、大抵先にやっちゃってる環境でいまから全部変えんの……?って思われないための方便。分類してあるものを潰すのも大変
苦しんできた頃に「ね、つらいでしょ?」ってもう一度言う
/components/buttons みたいな大分類はあってもよさそう
/provider /context
Context+Provider+hookまでコロケーションする
ひとつの方法でしか表示をしないんだったらそのコンポーネントまで一緒にする。Snackbarとかはこのパターン
code:plaintext
/components/Snackbar
/SnackbarContext.ts
/SnackbarProvider.tsx
/useSnackbar.ts
/index.tsx
Snackbar/Snackbar.tsx にするかは割と気まぐれ(参加してる人が納得する範囲で好みでいい)
表示までの責務を持たないものはちょっと迷うことがある
Providerは確かにコンポーネントだけどそれは従属物なんだから components ではないはず
Reactに依存してるので domains みたいなところには置きたくない
utils にいくかも
/tests /specs
これは作ることはあるが、E2Eを /test/e2e とやりたいときだけで、基本的に作らない
ユニットテスト・インテグレーションテストは foo.ts の隣に foo.test.ts を置く
/stories
Storybookもテストと同じようにコロケーションする
弱点がなくなるように・問題を増やさず苦しみが軽減されるように常に変更するのが肝
向いていないのはこんな状況
開発者を作業者扱いし考える余地を奪いたい場合には向いていない
考えたくない開発者も向いていないかも
常に機能開発に忙殺されていていわゆる「斧を研ぐ」ことができない環境にも向いていない
決めないことは怠慢か?→これは十分に決めている
結局ディレクトリ設計における「決め」というのは、必要に応じて改めるのが前提
プロダクト固有の関心や苦しみをより表現する形に常に改める、ために「変えるときの考え方」を決める
必要なのは、次のことが常に何かしらに記録されていること
「決めない」ことが決まっている、合意のもとで何もかもが変わりうる
変えるときの考え方はこうなっている
今は何をやっている
長期に手を放すときはこのあたりをガッとダンプしておく