Single-File Component
Vueの.vueファイルは、テンプレート、ロジック、スタイルを一つの部品にまとめます。<script setup>で宣言した変数や関数は、そのままテンプレートから使えます。
Badge.vueVUE
<script setup>
defineProps({ label: { type: String, required: true } })
</script>
<template>
<span class="badge">{{ label }}</span>
</template>
<style scoped>
.badge { padding: .25rem .5rem; border-radius: 8px; background: #e8f4fa; }
</style>scopedを付けると、そのコンポーネントの要素を中心にスタイルが適用されます。コンポーネント名は大文字始まりにし、役割が分かる名前を付けます。
Propsで親から値を渡す
Propsは親から子への入力です。子は読み取り専用として扱い、表示や計算に利用します。必須か、文字列か、数値かを定義すると間違いに気付きやすくなります。
ProductCard.vueVUE
<script setup>
const props = defineProps({
product: { type: Object, required: true }
})
</script>
<template>
<article>
<h2>{{ props.product.name }}</h2>
<p>{{ props.product.price }}円</p>
</article>
</template>親のテンプレートVUE
<ProductCard :product="{ name: 'ノート', price: 300 }" />Propsを直接変更しない
子が
子が
props.product.name = ...のように親のデータを変更すると、データの所有者が不明になります。変更はイベントで親へ依頼します。emitsでイベントを返す
子が「削除してほしい」「選択された」と伝えるには、defineEmitsでイベントを定義します。親は@イベント名で受け取ります。
子コンポーネントVUE
<script setup>
defineProps({ id: { type: Number, required: true } })
const emit = defineEmits(['remove'])
</script>
<template>
<button @click="emit('remove', id)">削除</button>
</template>親コンポーネントVUE
<TodoItem :id="todo.id" @remove="removeTodo" />
<script setup>
function removeTodo(id) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
</script>この構造では、配列を持つ親が削除を担当します。子はボタンの役割、親はデータ変更の役割を持つため、それぞれを独立して確認できます。
slotとコンポーネント間の設計
slotは、子コンポーネントの枠へ親から内容を差し込む仕組みです。カードの外枠だけを共通化し、タイトルや本文を呼び出し側で変えたい場合に使います。
Panel.vueと利用側VUE
<!-- Panel.vue -->
<section class="panel">
<slot />
</section>
<!-- 親 -->
<Panel>
<h2>お知らせ</h2>
<p>更新が完了しました。</p>
</Panel>同じコンポーネントへ双方向の入力を渡したい場合は、modelValueというPropsとupdate:modelValueイベント、またはdefineModelを使います。まずはPropsとemitsの一方向の流れを理解してから進むと混乱しません。