0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vueのスコープ付きスロットは`emit`と何が違う?

0
Last updated at Posted at 2026-08-11

今回は、Vueのスコープ付きスロット(slot props)を触りながら、propsemit・通常のslotとの役割の違いを整理したメモです。
子コンポーネントから親へ値が渡るように見えるので、emitと何が違うのかが最初はかなり混乱しました。

フロント側の画面の動きを理解して、コンポーネントごとの役割も少しずつ考えられるようになりたいです。
まだ途中段階なので、勘違いなどあれば教えていただけるとうれしいです。

今日やったこと

子が持つページ番号を使って、親側で表示する文章を切り替えました。

今回作ったのは、カード上部の「ボタン 1〜3」を押すと、カード本文の見出し・説明・ページ番号が切り替わる例です。

  • 子コンポーネントのBaseCard.vueが、現在のページ番号であるpageCountを持つ
  • 子のボタンを押すと、setPage()pageCountが変わる
  • 親コンポーネントのApp.vueが、ページ番号ごとの文章であるpagesを持つ
  • 子のpageCountをスロットプロップスとして親へ渡し、親が表示する本文を選ぶ

レコーディング 2026-08-11 161015.gif

ここをクリックすると動作をたしかめられます

キャラクター 「ボタンは子にあるのに、文章は親にある。操作を受け持つ場所と、内容を組み立てる場所が分かれているんだね」

Vueのスロットは、親が書いたHTMLやコンポーネントを子の中へ差し込むための仕組みです。さらに、子がスロットへ値を渡し、親がその値を受け取れる形がスコープ付きスロットです。 Vue.js公式ドキュメント ja.vuejs

今回の関係を先に図にすると、今の自分には少し見通しがよくなりました。

実線は今回のスコープ付きスロットの流れです。点線のpropsemitは今回のコードでは使っていませんが、同じページ切り替えを別の設計にした場合に登場する仕組みです。

子のBaseCard.vueでは、pageCountrefで管理しています。

<!-- src/components/BaseCard.vue -->

<script setup>
import { ref } from 'vue'

// このカード内で選択中のページ番号を管理する。
// ref の値を更新すると、関連する画面表示も更新される。
const pageCount = ref(1)

function setPage(page) {
  // 押されたボタンに対応するページ番号へ切り替える。
  pageCount.value = page
}
</script>

<template>
  <article class="base-card">
    <header class="base-card__header">
      <div class="base-card__buttons">
        <!-- pageCount と同じ番号のボタンだけ、選択中の見た目にする -->
        <button
          :class="pageCount === 1 ? 'btn btn--active' : 'btn'"
          @click="setPage(1)"
        >
          ボタン 1
        </button>

        <button
          :class="pageCount === 2 ? 'btn btn--active' : 'btn'"
          @click="setPage(2)"
        >
          ボタン 2
        </button>

        <button
          :class="pageCount === 3 ? 'btn btn--active' : 'btn'"
          @click="setPage(3)"
        >
          ボタン 3
        </button>
      </div>
    </header>

    <div class="base-card__body">
      <!--
        子が持つ pageCount を、親が書いたスロット内容へ渡す。
        親側では v-slot="{ pageCount }" として受け取れる。
      -->
      <slot :page-count="pageCount">
        <!-- 親から内容が渡されなかった場合だけ表示する -->
        <p>スロットが渡されていません。</p>
      </slot>
    </div>
  </article>
</template>

親のApp.vueでは、子から受け取ったpageCountを使って、表示する文章を選びます。

<!-- src/App.vue -->

<script setup>
import BaseCard from './components/BaseCard.vue'

// ページ番号ごとに表示する内容を親コンポーネントで管理する。
const pages = {
  1: {
    title: 'ページ 1',
    body: '最初のページです。基本情報を確認してください。',
  },
  2: {
    title: 'ページ 2',
    body: '2ページ目です。詳細な設定内容を確認してください。',
  },
  3: {
    title: 'ページ 3',
    body: '最後のページです。完了してよければ送信してください。',
  },
}
</script>

<template>
  <section class="card-list">
    <h1>スコープ付きスロット サンプル</h1>

    <!--
      子が渡した pageCount をスロットプロップスとして受け取る。
      受け取った番号に対応する pages の内容を表示する。
    -->
    <BaseCard v-slot="{ pageCount }">
      <div class="page-content">
        <h2>{{ pages[pageCount].title }}</h2>
        <p>{{ pages[pageCount].body }}</p>

        <!-- テンプレート内では ref の .value を書かずに値を使える -->
        <p class="page-indicator">
          現在のページ: {{ pageCount }} / 3
        </p>
      </div>
    </BaseCard>
  </section>
</template>

画面上では、子にあるボタンを押すとpageCountが変わり、その番号を受け取った親の本文が切り替わります。子が操作と状態を持ち、親が状態に対応する表示内容を持つ構成になっています。

つまずいたところ

slot propsが「子から親へ値を渡す」ように見えて、emitと何が違うのかが一番混乱しました。

emitは子から親へ値を渡す仕組みではないのか

最初は、emitが「子から親へ簡単にデータを書き換えさせないための仕組み」なのだと思っていました。

今の理解では、emitは子が親に対して、操作や出来事を通知するための窓口です。子が親の状態を直接変更するのではなく、「ボタンが押されました」「このページが選ばれました」と親へ伝え、親が次の処理を決めます。 qiita

たとえば、親がページ番号を管理するなら、子は次のようにイベントを送れます。

<!-- 子:ページ番号を親へ直接変更せず、操作内容を通知する -->

<script setup>
// 子が親へ送るイベント名を定義する。
const emit = defineEmits(['change-page'])

function selectPage(page) {
  // 「page を選択した」という出来事を親へ送る。
  emit('change-page', page)
}
</script>

親では、イベントを受け取ってページ番号を更新します。

<!-- 親:子からの通知を受けて、自分が持つ状態を更新する -->

<script setup>
import { ref } from 'vue'
import BaseCard from './components/BaseCard.vue'

// 現在のページ番号を親で管理する。
const pageCount = ref(1)

function changePage(page) {
  // 子から受け取った番号を親の状態へ反映する。
  pageCount.value = page
}
</script>

<template>
  <!-- 子が change-page を emit したら、親の changePage を実行する -->
  <BaseCard @change-page="changePage" />
</template>

メニューのボタンが押されたとき、ボタン自身が画面全体の選択状態を勝手に書き換えるのではなく、「2番目が選ばれた」とメニュー画面へ伝える感覚に近いです。親はその通知を受け取り、選択中の項目を2番へ変えるか、確認画面を出すかを決められます。

キャラクター 「`emit`は“値を返すもの”というより、“この操作が起きたよ”と親へ知らせるものなんだね」

slot propsは親が子の状態を変更するためのものではない

今回のslot propsは、イベントの通知ではありません。

<!-- 子:現在のページ番号を、親のスロット内容で使えるようにする -->

<slot :page-count="pageCount">
  <p>スロットが渡されていません。</p>
</slot>

このコードは、子が「現在のページ番号はこれです」と親が書いたスロット内容へ情報を渡しているだけです。親は受け取ったpageCountを使って、見出しや説明文を選べます。

ただし、このpageCountを親が直接変更できるわけではありません。今回の構成では、pageCountの管理者はBaseCardです。

今の理解(メモ)

4つの仕組みは、データの向きだけではなく、「何を渡すのか」「誰が状態を管理するのか」「誰が最終的な表示を決めるのか」で見ると整理しやすそうです。

1. props:親が子へ設定や材料を渡す

propsは、親コンポーネントが持つ値を子へ渡す仕組みです。子は受け取った値を使って、自分の表示や動きを決めます。

たとえば、親がカードのタイトル・色・画像URL・初期ページ番号を持っていて、子に表示を任せたい場合に使えそうです。

<!-- 親:子にカードのタイトルを渡す -->

<BaseCard title="設定内容" />
<!-- 子:親から渡された title を表示する -->

<script setup>
// 親から渡される値を props として定義する。
defineProps({
  title: String,
})
</script>

<template>
  <!-- 子は受け取った title を表示に使う -->
  <h2>{{ title }}</h2>
</template>

キャラクターへ名前・能力値・装備の初期設定を渡すように、親が決めた内容を子へ渡すイメージです。子は渡された設定を使って表示できますが、元の設定を決める責任は親にあります。

2. emit:子が親へ起きたことを知らせる

emitは、子で起きた操作や変化を親へ知らせる仕組みです。データそのものを一方的に書き換えるというより、「この操作がありました」と親に判断を任せるための通知に見えます。

たとえば、子にある削除ボタンがクリックされたとき、子は「削除してほしいID」を親へ送れます。親は受け取ったあと、本当に削除するか、確認ダイアログを出すか、削除処理を進めるかを決めます。

3. 通常のslot:親が子の中へ画面の中身を差し込む

通常のslotは、子コンポーネントに用意された表示場所へ、親が書いたHTMLや他のコンポーネントを差し込む仕組みです。

propsが文字列・数値・配列・オブジェクトのようなデータを渡すものなのに対し、slotは見出し・ボタン・画像・コンポーネントなど、画面の中身そのものを渡す用途に向いていそうです。Vue公式の用語集でも、propsはデータ値、スロットはHTML要素やVueコンポーネントを含むリッチなコンテンツを渡すものとして区別されています。 Vue.js公式ドキュメント ja.vuejs

<!-- 子:親が内容を置ける場所を用意する -->

<template>
  <article class="base-card">
    <slot>
      <!-- 親が内容を渡さなかったときの予備表示 -->
      <p>カードの内容がありません。</p>
    </slot>
  </article>
</template>

同じウィンドウ枠を使いながら、場面ごとに「アイテム説明」「セーブ確認」「設定一覧」のような異なる中身を入れ替える感覚に少し近いです。

4. slot props:子の状態を使って、親がスロットの中身を描画する

slot propsは、通常のslotに「子が持つ値を親に渡す」役割を加えたものです。

今回のBaseCardでは、子がpageCountを管理しています。しかし、ページ番号に対応した見出しと説明文は親で書きたいです。そのため子は、<slot>pageCountを渡しています。

<!-- 親:子から受け取った pageCount を使って表示を決める -->

<BaseCard v-slot="{ pageCount }">
  <!-- 子の状態に応じて、親が書いた内容を切り替える -->
  <h2>{{ pages[pageCount].title }}</h2>
  <p>{{ pages[pageCount].body }}</p>
</BaseCard>

ここで大事なのは、親がpageCountを受け取っても、状態の管理者は子のままだという点です。親はpageCountを読んで表示を決められますが、今回のコードでは親から子のページ番号を変更することはできません。

共通のメニュー部品が「現在選ばれている項目は2番目です」と親へ知らせ、親がその番号に対応する説明文を表示するような関係に見えます。共通部品は選択状態を管理し、親は画面ごとに違う説明や見た目を組み立てられます。

キャラクター 「slot propsは、部品の中で起きていることを少し共有して、画面ごとの見せ方は外側に任せる感じなんだね」

次にやりたいこと

次は、ページ番号を親と子のどちらが持つかによって、実装の流れがどう変わるかを試したいです。

  • 親がpageCountを持つpropsemit構成を作る
    子のボタン操作が、親の状態更新と本文表示の切り替えにつながる流れを確認したい
  • v-modelを使って、ページ番号を親子間で扱う例を作る
    propsと更新イベントをセットで扱うときの書き方を試したい
  • slot propsでpageCount以外にsetPageも渡せるか試す
    親が独自の「前へ」「次へ」ボタンを置けるのか確認したい
  • 同じBaseCardへ複数の異なる本文を渡す
    コンポーネントの共通部分と、画面ごとに変える部分の分け方を確かめたい

今日の整理

4つの仕組みを、渡すものと状態の管理者でまとめ直しました。

用語 何を渡すか 主な役割 状態の管理者
props 親が持つデータや設定 子へ表示内容・初期値を渡す 基本的に親
emit 子で起きた操作や出来事 親へ処理の判断を依頼する 親が更新を判断する
slot HTMLやVueコンポーネント 子の中へ親が画面内容を差し込む 内容は親、枠は子
slot props 子の状態を含む値 親が子の状態を使って表示を作る 今回は子

4つを同じ親子関係の中へ置くと、値・イベント・HTMLの流れる向きがそれぞれ違うことが分かりやすそうです。

今回のコードで実際に使っているのは、通常のslotとslot propsです。親がpageCountを管理する形へ変えた場合には、propsemitを使う構成になりそうです。

おわりに

今回は、スコープ付きスロットはemitの代わりではなく、子が持つ状態を使って親が表示を組み立てるための仕組みだと少し整理できました。

Mermaid図にしてみると、データ・イベント・HTMLの流れる向きが同じではないことも見えてきました。まだ「この画面ならslot propsを使う」とすぐ判断できるわけではありませんが、状態を管理する場所と表示を決める場所を分けて考える視点は持てそうです。

学習中の内容なので、勘違いや理解不足もあると思います。もし気になる点があれば教えてもらえるとうれしいです。

 注意事項
 本ブログに掲載している内容は、私個人の見解であり、
 所属する組織の立場や戦略、意見を代表するものではありません。
 あくまでエンジニアとしての経験や考えを発信していますのでご了承ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?