公開日
- 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>
ポン吉: 「defineEmits
!defineProps
と似た名前の関数ですね!これで、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>
は、コンポーネントに柔軟な『差し込み口』を作るための仕組みなんじゃ。これにより、レイアウトとコンテンツを分離でき、非常に見通しが良く、再利用性の高いコンポーネント設計が可能になる。」
ポン吉は、Props
、Emits
、そしてSlots
という、コンポーネントを自在に操るための三種の神器を手に入れた気がした。
博士: 「さて、ポン吉よ。君はもう、VueとTypeScriptの基本的な魔法は、ほとんど習得したと言ってよかろう。残すは最終試験のみじゃ。」
ポン吉: 「最終試験…!?」
博士: 「うむ。次回は、これまで学んだことの総仕上げじゃ。君自身の手で、型安全なVueアプリケーションを一から作ってもらう。それができれば、君ももう立派なフロントエンドエンジニアの卵じゃよ。」
ポン吉: 「はい!やってみます!ポン!」
ポン吉の胸は、期待と少しの不安で高鳴っていた。
🌟 今日のまとめ
defineEmits
を使うと、子から親へ渡すイベントに型を付けられ、安全なコミュニケーションができる。<slot>
は、コンポーネントに外部からコンテンツを差し込むための「場所取り」タグ。Slots
を使うことで、レイアウトとコンテンツを分離し、再利用性の高いコンポーネントを作れる。Props
,Emits
,Slots
は、柔軟なコンポーネント設計のための三種の神器。
次回予告 「博士からの卒業証書」【最終話】
いよいよ最終回!ポン吉がこれまで学んだ全ての知識を総動員し、型安全なVueアプリケーションの構築に挑戦します。果たして、ポン吉は博士から卒業証書を受け取ることができるのでしょうか?
👨🏫 博士からの補足
ポン吉がProps
、Emits
、Slots
という三種の神器を手に入れて喜んでおったが、実はこれらの真の価値は、単なる機能ではなく、その背後にある「コンポーネント設計の哲学」にあるんじゃ。
最終話を前に、この哲学について少し話しておこう。
🌊 単方向データフローという川の流れ
まず、大切なのは「データは川のように一方向に流れる」という考え方じゃ:
祖父コンポーネント ← 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話 おわり