frontendBaby

公開日

- 10 分で読めます

第12話 「EmitsとSlotsの世界」


どうしてもVue.jsが分からないので森の博士の研究所に押しかけてみたら、人生が激変した子タヌキの話

この物語は、フロントエンド技術を楽しく学ぶことを目的に、生成AIを活用して執筆されています。 技術的な情報の正確性には細心の注意を払っていますが、その内容がすべて真実であることを保証するものではありません。 あくまで学習の補助ツールとして、肩の力を抜いてお楽しみください。


登場人物紹介

  • フロントエンド博士: 森の奥の研究所に住む、フロントエンドのことなら何でも知っている物知り博士。ポン吉の素朴な疑問にいつも優しく(そして面白おかしく)答えてくれる。
  • ポン吉: 好奇心旺盛な子タヌキ。将来の夢はフロントエンドエンジニア。最近Vue.jsを学び始めたが、その奥深さに興味津々。新しい知識をゲットすると、思わず「ポン!」と飛び跳ねる特技あり。

第12話🦝「EmitsとSlotsの世界」

<script setup>という強力な魔法をマスターし、コンポーネント作りに自信をつけたポン吉。しかし、彼の探求心は尽きることがない。

ポン吉: 「博士!propsで親から子へデータを渡せるのは分かりました。でも、子から親に何かを伝えたい時は、どうすればいいんですか?」

博士: 「素晴らしい質問じゃ、ポン吉!それこそが、今日学ぶ一つ目のテーマ、『Emits』じゃ。子はイベントを『発行(emit)』し、親はそれを『購読(listen)』する。これがVueにおける子から親へのコミュニケーションの基本じゃよ。」

博士は、子がボタンを持ち、親にクリックを通知するコンポーネントの例を書き始めた。

// 子コンポーネント: MyButton.vue
<script setup lang="ts">
// defineEmitsで、発行するイベントとその型を定義する
const emit = defineEmits<{ (e: 'notify', message: string): void }>()

function handleClick() {
  // 定義したイベントを発行する
  emit('notify', 'ボタンが押されました!')
}
</script>

<template>
  <button @click="handleClick">通知ボタン</button>
</template>
// 親コンポーネント: App.vue
<script setup lang="ts">
import MyButton from './MyButton.vue'

function handleNotification(message: string) {
  alert(message)
}
</script>

<template>
  <MyButton @notify="handleNotification" />
</template>

ポン吉: 「defineEmitsdefinePropsと似た名前の関数ですね!これで、notifyという名前で、string型のメッセージを一緒に送るイベントを定義しているんですね!」

博士: 「その通りじゃ!defineEmitsで型を定義しておくことで、もしemit('notify', 123)のように間違った型のデータを渡そうとすると、TypeScriptが『型が違うよ!』と厳しく教えてくれる。これで、親子間の連絡ミスがなくなるんじゃ。」

ポン吉: 「なるほど!子から親への連絡も、型で安全にできるんですね!ポン!」

博士: 「うむ。では、もう一つのテーマ、『Slots』に進もうかの。これは、コンポーネントの『再利用性』を飛躍的に高める魔法じゃ。」

ポン吉: 「スロット…?」

博士: 「例えば、素敵なデザインの『カード』コンポーネントを作ったとしよう。しかし、カードの中身は、ある時は文章、ある時は画像と、使う場所によって変えたい。そんな時にSlotsが役立つんじゃ。」

博士は、汎用的なカードコンポーネントを示した。

// 子コンポーネント: BaseCard.vue
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header"></slot>
    </div>
    <div class="card-content">
      <slot></slot> <!-- nameがないのがデフォルトスロット -->
    </div>
  </div>
</template>

<style scoped>
.card { border: 1px solid #ccc; border-radius: 8px; padding: 16px; }
.card-header { font-weight: bold; }
</style>
// 親コンポーネント: App.vue
<BaseCard>
  <template #header>
    <h3>ポン吉のプロフィール</h3>
  </template>

  <template #default>
    <p>ぼく、ポン吉!フロントエンドエンジニアになるのが夢です!</p>
  </template>
</BaseCard>

ポン吉: 「すごい!<slot>というタグが、親コンポーネントで書いた内容の『場所取り』をしているんですね!これなら、カードの骨組みは再利用しつつ、中身だけを自由に入れ替えられます!」

博士: 「その通りじゃ!<slot>は、コンポーネントに柔軟な『差し込み口』を作るための仕組みなんじゃ。これにより、レイアウトとコンテンツを分離でき、非常に見通しが良く、再利用性の高いコンポーネント設計が可能になる。」

ポン吉は、PropsEmits、そしてSlotsという、コンポーネントを自在に操るための三種の神器を手に入れた気がした。

博士: 「さて、ポン吉よ。君はもう、VueとTypeScriptの基本的な魔法は、ほとんど習得したと言ってよかろう。残すは最終試験のみじゃ。」

ポン吉: 「最終試験…!?」

博士: 「うむ。次回は、これまで学んだことの総仕上げじゃ。君自身の手で、型安全なVueアプリケーションを一から作ってもらう。それができれば、君ももう立派なフロントエンドエンジニアの卵じゃよ。」

ポン吉: 「はい!やってみます!ポン!」

ポン吉の胸は、期待と少しの不安で高鳴っていた。


🌟 今日のまとめ

  • defineEmits を使うと、子から親へ渡すイベントに型を付けられ、安全なコミュニケーションができる。
  • <slot> は、コンポーネントに外部からコンテンツを差し込むための「場所取り」タグ。
  • Slotsを使うことで、レイアウトとコンテンツを分離し、再利用性の高いコンポーネントを作れる。
  • Props, Emits, Slotsは、柔軟なコンポーネント設計のための三種の神器。

次回予告 「博士からの卒業証書」【最終話】

いよいよ最終回!ポン吉がこれまで学んだ全ての知識を総動員し、型安全なVueアプリケーションの構築に挑戦します。果たして、ポン吉は博士から卒業証書を受け取ることができるのでしょうか?

👨‍🏫 博士からの補足

ポン吉がPropsEmitsSlotsという三種の神器を手に入れて喜んでおったが、実はこれらの真の価値は、単なる機能ではなく、その背後にある「コンポーネント設計の哲学」にあるんじゃ。

最終話を前に、この哲学について少し話しておこう。

🌊 単方向データフローという川の流れ

まず、大切なのは「データは川のように一方向に流れる」という考え方じゃ:

祖父コンポーネント ← Events(最終的にここで処理)
  ↓ Props(データが流れる) ↑ Events
父コンポーネント  ← Events(ここを通過)
  ↓ Props(さらに流れる) ↑ Events
子コンポーネント ← Events(ここから発行)

この “Props down, Events up” という黄金律は、ただのルールではない。これは「予測可能性」という、大規模なアプリケーションの生命線を守るための智恵なんじゃ。

⚖️ 適度な抽象化という芸術

そして、Slotsを使った再利用可能なコンポーネント設計では、「適度な抽象化」が鍵になる。

<!-- ❌ 抽象化しすぎ:何でもできるが、何をするコンポーネントか分からない -->
<SuperFlexibleComponent 
  :config="complexConfig" 
  :behaviors="allBehaviors" 
  :styles="everythingStyle" 
/>

<!-- ✅ 適度な抽象化:目的が明確で、柔軟性もある -->
<UserCard>
  <template #avatar>
    <img :src="user.avatarUrl" />
  </template>
  <template #content>
    <h3>{{ user.name }}</h3>
    <p>{{ user.bio }}</p>
  </template>
</UserCard>

過度な抽象化は毒じゃ。「何でもできるコンポーネント」は、結果的に「何をするコンポーネントか分からない」という混乱を生む。

🎯 責務分離という美学

最後に、コンポーネントの「責務分離」について。それぞれのコンポーネントは、明確な役割を持つべきじゃ:

  • Presentational Component:見た目だけに集中する(UIの責務)
  • Container Component:データの管理に集中する(ロジックの責務)
  • Layout Component:配置だけに集中する(レイアウトの責務)
<!-- ✅ 責務が明確 -->
<UserProfile>           <!-- Container: データ管理 -->
  <ProfileLayout>       <!-- Layout: 配置 -->
    <UserAvatar />      <!-- Presentational: 見た目 -->
    <UserInfo />        <!-- Presentational: 見た目 -->
  </ProfileLayout>
</UserProfile>

第12話 おわり