Slots - 親が描画責務を差し込むコンポーネント拡張ポイント
ロードマップ: Vue.js学習ロードマップ
What(何についてか)
Slots は、子コンポーネントが自分のテンプレート内に「親が描画内容を差し込める拡張ポイント」を定義する仕組みである。
props が「値を子へ渡す契約」であるのに対し、slots は「描画内容を子へ差し込む契約」である。
Vue の slots は段階的に以下へ拡張される。
- default slot: 1つの差し込み口を持つ
- named slots: 複数の差し込み口を持つ
- scoped slots: 子のデータを親へ渡しつつ、親に描画責務を委ねる
- renderless component: 描画をほぼ持たず、ロジックだけを提供する
Why(なぜ必要か)
コンポーネント設計では、子が持つべき責務と親へ開放すべき責務を分離する必要がある。
- props だけでは、子の見た目まで固定されやすい
- slots があると、共通の外枠や振る舞いを子に閉じ込めつつ、中身の描画だけ親に委ねられる
- named slots により、header, content, footer のような複数領域を持つレイアウト部品を自然に設計できる
- scoped slots により、子が保持するデータと親が保持する描画責務を接続できる
特に scoped slots は、一覧取得・ページネーション・状態管理などのロジックを子に閉じ込めつつ、各 item の見た目だけを親ごとに差し替えたい場合に有効である。
How(どう動くか)
1. default slot
子は <slot></slot> を置き、親がその中身を渡す。
<!-- parent -->
<FancyButton>
Click me!
</FancyButton><!-- child -->
<button class="fancy-btn">
<slot></slot>
</button>この構造では、子が外枠の button とスタイルを担当し、親が内部コンテンツを担当する。
2. Render Scope
slot content は親テンプレートで定義されるため、評価スコープも親に属する。
- 親テンプレート内の式は親の state を参照する
- 子テンプレート内の式は子の state を参照する
- slot の描画位置が子の中にあっても、slot content は子の state を直接参照できない
この制約が、後述する scoped slots の必要性につながる。
3. fallback content
親が slot content を渡さない場合に備え、子側で既定表示を持てる。
<button type="submit">
<slot>Submit</slot>
</button>これは props の default 値に近いが、対象は「値」ではなく「描画内容」である。
4. named slots
複数の差し込み口を持つには、子が name 属性付きの <slot> を定義する。
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>親は v-slot または # 省略記法で、どの内容をどこへ差し込むかを指定する。
<BaseLayout>
<template #header>
<h1>Page Title</h1>
</template>
<p>Main content</p>
<template #footer>
<p>Contact info</p>
</template>
</BaseLayout>レイアウトコンポーネントや Card / Modal のような複数領域部品では特に有効である。
5. conditional slots
slot が渡されたときだけ wrapper を描画したい場合は $slots を使う。
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>これにより、空の header / footer DOM を残さずに済む。
6. scoped slots
通常の slot では親スコープしか見えないため、子が保持するデータを使って親に描画させたい場合は、子が slot props を渡す。
<!-- child -->
<slot :text="greetingMessage" :count="1"></slot><!-- parent -->
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>これは「子が持つデータ」を「親が持つ描画責務」に接続する仕組みである。
FancyList パターン
scoped slots の代表例は FancyList である。
- 子は API 取得, pagination, loading などのロジックを持つ
- 子は
v-forで items を反復する - 各 item の表示方法だけを親へ委譲する
<!-- parent -->
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList><!-- child -->
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" v-bind="item"></slot>
</li>
</ul>この設計では、ロジックの共通化と描画の差し替えを両立できる。
7. Renderless Components
scoped slots をさらに推し進めると、描画構造をほぼ持たず、ロジックだけを提供する renderless component に到達する。
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>このパターンは、以下の 3 層構造として理解できる。
flowchart TD A["A. ロジック抽象層\n取得・監視・状態管理"] --> B["B. 業務UI部品層\nslot を具体化した部品"] B --> C["C. 画面層\n部品を配置して利用する親"]
- A: 汎用ロジックを持つ層
- B: A の slot を埋めて意味のある部品にする層
- C: B を実際の画面で使う層
Vue 3 では、この A 層の多くは renderless component ではなく composable に置き換え可能である。
props と slots の採用判断
props を優先する場合
- 子が受け取るのが「値」である
- 子が描画責務を持つ
- API を厳密に制約したい
- 表示パターンが固定的である
slots を使う場合
- 親が描画内容を差し込みたい
- 子は外枠や配置だけを担当する
- 複数領域を持つレイアウト部品を作りたい
- 子のデータを親に渡しつつ描画を委ねたい
実務では、まず props で表現できるなら props を優先し、必要になった時だけ slots を開放する方が API は安定しやすい。
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| ロジック再利用 | renderless component や mixins が選択肢になりやすい | composable が第一候補になる |
| slot の位置づけ | 柔軟な描画差し替え手段 | 同じだが、ロジック再利用は composable と役割分担する |
| 複雑な責務分離 | component ネストが増えがち | composable + slots でより局所的に分離しやすい |
Key Concepts
| 用語 | 説明 |
|---|---|
| default slot | 名前なしの基本差し込み口 |
| named slots | 複数の意味的な差し込み口 |
| fallback content | 親が渡さない時の既定描画 |
| render scope | slot content は親スコープで評価されるという原則 |
| scoped slots | 子のデータを親へ渡しつつ描画を委ねる仕組み |
| renderless component | 描画を持たず、ロジックだけを提供する component |
| composable | Composition API でロジックを再利用する関数ベースの仕組み |