はじめに
この記事では、フロントエンドのコンポーネントや関連ファイルを、どのディレクトリに置くかを整理します。
フロントエンドのコードを書いていると、次のような迷いが出ます。
-
Buttonはどこに置くのか -
SearchFormは共通コンポーネントなのか、機能専用コンポーネントなのか -
UserListはコンポーネントなのか、画面なのか -
pagesとcomponentsは何が違うのか -
featuresディレクトリは何のためにあるのか
小さいアプリでは、どこに置いても大きな問題になりません。
しかし画面数や機能が増えると、components に何でも入っている状態が読みづらさにつながります。
この記事では、Vue / Nuxt を例にしながら、components/ui、features、pages の分け方を中心に整理します。
考え方は React / Next.js でも大きくは変わりません。
先に結論
コンポーネントの置き場所は、見た目の粒度だけで決めるより、責務と変更単位で決める方が実務では扱いやすいです。
特に重要なのは、components に何でも入れないことです。
基本は次のように考えます。
| 置き場所 | 置くもの | 判断基準 |
|---|---|---|
components/ui |
汎用 UI 部品 | 業務名を知らなくても使える |
features/*/components |
機能専用の UI 部品 | 特定の業務機能と一緒に変更される |
pages |
ルーティングに対応する画面 | URL と対応する |
composables / hooks
|
状態や処理 | UI ではなく振る舞いを再利用する |
utils |
汎用関数 | 画面や機能に依存しない |
types |
型定義 | 共有範囲に合わせて置く |
迷ったときは、次の順番で考えると整理しやすいです。
- URL に対応するなら
pages - 特定機能のための部品なら
features - 業務名を知らなくても使える UI なら
components/ui - 見た目ではなく処理なら
composables/hooks - どこからでも使える純粋な処理なら
utils
この順番で見ると、単に「コンポーネントだから components」という置き方を避けやすくなります。
pages は画面の入口
pages は、ルーティングに対応する画面を置く場所です。
Nuxt では、pages ディレクトリに置いたファイルからルートが生成されます。
pages/
users/
index.vue
[id].vue
この場合、pages/users/index.vue はユーザー一覧画面に対応します。
pages/users/[id].vue はユーザー詳細画面に対応します。
pages の主な責務は、画面全体を組み立てることです。
- URL パラメータを受け取る
- 画面単位のデータ取得を呼び出す
- レイアウトを指定する
-
featuresやcomponents/uiを配置する - 画面全体の状態をつなぐ
逆に、pages に細かい UI 実装を大量に書くと、画面ファイルが肥大化します。
検索フォーム、一覧テーブル、ページネーション、モーダルの中身まで pages/users/index.vue に直接書くと、後から読むのがつらくなります。
pages は画面の入口です。
細かい見た目や機能の中身は、別の場所に切り出す方が扱いやすいです。
components/ui は汎用 UI を置く場所
components/ui は、アプリ全体で使い回せる UI 部品を置く場所です。
ここでのポイントは、「コンポーネントだから全部 components」ではないことです。
components/ui に置きやすいのは、業務名を知らなくても使える部品です。
components/
ui/
Button.vue
TextInput.vue
Checkbox.vue
Modal.vue
Badge.vue
たとえば Button は「保存ボタン」ではなく、ただのボタンです。
<Button>保存</Button>
<Button variant="danger">削除</Button>
この Button は、ユーザー管理でも、商品管理でも、設定画面でも使えます。
そのため components/ui に置くのが自然です。
一方で、次のような部品は注意が必要です。
components/
UserSearchForm.vue
UserTable.vue
UserPermissionBadge.vue
これらはコンポーネントではありますが、汎用 UI ではありません。
User という業務名を含んでおり、ユーザー機能の知識を持っています。
この場合は、components 直下より features/user 配下に置く方が分かりやすいです。
features は機能単位でまとめる場所
features は、機能単位でコードをまとめる場所です。
ユーザー検索機能であれば、次のようにまとめられます。
features/
user/
components/
UserSearchForm.vue
UserTable.vue
UserStatusBadge.vue
composables/
useUserSearch.ts
api/
userApi.ts
types/
user.ts
この構成では、ユーザー機能に関係するものが features/user に集まります。
- ユーザー検索フォーム
- ユーザー一覧テーブル
- ユーザー API
- ユーザー検索の状態管理
- ユーザー型
業務アプリでは、機能ごとに変更が入ることが多いです。
フォームの項目が変わると、API パラメータ、型、一覧表示、検索状態も一緒に変わることがあります。
そのため、機能に閉じたコードは近くに置いた方が変更しやすくなります。
features に置くか迷ったら、次の質問をします。
- その部品名に業務名が入っているか
- その部品は特定画面や特定機能でしか使わないか
- API、型、状態管理と一緒に変更されるか
この答えが「はい」に寄るなら、features 配下に置く方が自然です。
Atomic Design は分類より依存関係を見る
Atomic Design は、UI を小さい部品から大きい部品へ組み立てる考え方です。
代表的には、次の分類があります。
atomsmoleculesorganismstemplatespages
この考え方は、UI の粒度を整理するには便利です。
ただし、実務で厳密に分類しようとすると迷う場面もあります。
たとえば SearchBox は molecule なのか。
検索条件が業務に依存するなら feature の component なのか。
ここで重要なのは、Atomic Design の名前に合わせることではありません。
そのコンポーネントが何に依存しているかを見ることです。
- 業務を知らない UI なら
components/ui - 業務機能に依存するなら
features/*/components - URL に対応するなら
pages
Atomic Design は否定しなくてよいです。
ただし、ディレクトリ名を分類表に合わせるより、責務と変更単位を見る方が実務では迷いにくくなります。
organisms と features は混ざりやすい
Atomic Design を使うと、organisms に何を置くかで迷いやすくなります。
たとえば次のような部品です。
components/
organisms/
UserSearchPanel.vue
UserTable.vue
これらは画面の一部としては organisms に見えます。
一方で、User という業務機能にも強く依存しています。
この場合、実務では次のように置いた方が変更に強いことがあります。
features/
user/
components/
UserSearchPanel.vue
UserTable.vue
理由は単純です。
ユーザー検索の仕様変更が入ったとき、フォーム、テーブル、API、型、状態管理をまとめて見たいからです。
見た目の粒度で分けるより、何と一緒に変更されるかで分けた方が管理しやすい場面は多いです。
composables / hooks は処理を置く場所
Vue では composables、React では hooks という名前で処理を切り出すことがあります。
ここに置くのは UI そのものではありません。
状態や処理です。
composables/
useModal.ts
usePagination.ts
機能に閉じた処理であれば、グローバルな composables や hooks ではなく、features 配下に置く選択もあります。
features/
user/
composables/
useUserSearch.ts
useUserSearch は、ユーザー検索に依存しています。
そのため、アプリ全体の共通処理として扱うより、ユーザー機能の中に置く方が自然です。
一方で、useModal や usePagination のように複数機能で使えるものは、共通の composables / hooks に置いてもよいです。
utils と types は共有範囲で決める
utils には、画面や機能に依存しない汎用処理を置きます。
utils/
formatDate.ts
formatCurrency.ts
sleep.ts
特定機能でしか使わない処理は、features の中に置く方が自然です。
features/
user/
utils/
normalizeUserSearchParams.ts
types も同じです。
複数機能で使う型だけをトップレベルに置き、特定機能でしか使わない型は機能配下に置きます。
types/
api.ts
pagination.ts
features/
user/
types/
user.ts
userSearch.ts
判断基準は共有範囲です。
- 複数機能で使うならトップレベル
- 特定機能で使うなら
features/* - そのコンポーネントだけで使うなら同じファイル内でもよい
最初からすべてを共通化する必要はありません。
共有範囲が広がった時点で移動しても十分です。
layouts は共通の外枠
layouts は、複数画面で共通する外枠を置く場所です。
layouts/
default.vue
admin.vue
ヘッダー、サイドバー、フッターなど、複数画面で共通する構造を置きます。
pages は画面ごとの中身を組み立てる場所です。
layouts は複数画面で使う外枠を持つ場所です。
この2つを分けると、画面ごとの処理とアプリ全体の構造を混ぜずに済みます。
実務で扱いやすい構成例
Atomic Design を厳密にディレクトリ名へ反映するより、次のような構成の方が扱いやすいことが多いです。
components/
ui/
Button.vue
TextInput.vue
Modal.vue
features/
user/
components/
UserSearchForm.vue
UserTable.vue
composables/
useUserSearch.ts
api/
userApi.ts
types/
user.ts
pages/
users/
index.vue
[id].vue
layouts/
default.vue
composables/
useModal.ts
utils/
formatDate.ts
この構成では、汎用 UI は components/ui に置きます。
ユーザー機能に関係するものは features/user に置きます。
URL に対応する画面は pages に置きます。
pages/users/index.vue は、次のように画面を組み立てるだけに寄せます。
<script setup lang="ts">
import UserSearchForm from "~/features/user/components/UserSearchForm.vue";
import UserTable from "~/features/user/components/UserTable.vue";
import { useUserSearch } from "~/features/user/composables/useUserSearch";
const {
conditions,
users,
pending,
search,
} = useUserSearch();
</script>
<template>
<section>
<UserSearchForm
v-model="conditions"
@submit="search"
/>
<UserTable
:users="users"
:pending="pending"
/>
</section>
</template>
この形にすると、pages は画面の入口として読みやすくなります。
検索フォームや一覧テーブルの詳細は、ユーザー機能の中を見れば分かります。
置き場所に迷ったときの判断表
迷いやすい例を表で整理します。
| ファイル | 置き場所の例 | 理由 |
|---|---|---|
Button.vue |
components/ui |
業務に依存しない汎用 UI |
TextInput.vue |
components/ui |
複数機能で使える入力部品 |
UserSearchForm.vue |
features/user/components |
ユーザー検索に依存する |
UserTable.vue |
features/user/components |
ユーザー一覧の表示仕様を持つ |
useUserSearch.ts |
features/user/composables |
ユーザー検索の状態と処理を持つ |
useModal.ts |
composables |
複数機能で使える処理 |
formatDate.ts |
utils |
機能に依存しない汎用関数 |
user.ts |
features/user/types |
ユーザー機能の型 |
pagination.ts |
types |
複数機能で共有する型 |
pages/users/index.vue |
pages |
URL に対応する画面 |
default.vue |
layouts |
複数画面で共通する外枠 |
判断に迷ったら、次の一文で説明できるかを見ます。
「このファイルは、何の責務を持っているのか」
この説明に業務名が必要なら features。
URL の説明になるなら pages。
業務名なしで UI 部品として説明できるなら components/ui。
この分け方にすると、ディレクトリ名に振り回されにくくなります。
まとめ
フロントエンドのコンポーネント構成では、ディレクトリ名そのものより責務と変更単位が重要です。
components/ui は、汎用 UI を置く場所です。
features は、業務機能に関係するコードをまとめる場所です。
pages は、ルーティングに対応する画面を置く場所です。
Atomic Design の atoms / molecules / organisms / pages は、UI の粒度を整理する考え方として便利です。
ただし、実務では業務機能ごとに変更が入ることも多いため、features と組み合わせて考える方が管理しやすくなります。
最終的には、次のように整理すると分かりやすいです。
- 汎用的な見た目の部品は
components/ui - 業務機能に関係する部品は
features/*/components - 画面そのものは
pages - 状態や処理は
composables/hooks - 汎用処理は
utils - 共通レイアウトは
layouts - 型定義は共有範囲に合わせて置く
正解のディレクトリ構成は、プロジェクトによって変わります。
ただし、「このファイルは何の責務を持っているのか」「何と一緒に変更されるのか」を説明できる構成にしておくと、後から読む人にも変更する人にも扱いやすいフロントエンドになります。
関連して、コードを小さな単位へ分ける考え方は「プログラミングは分割統治で考えると扱いやすくなる」でも整理しています。