Дані йдуть вниз через props, події - вгору через emits. Це односпрямований потік даних.
<!-- Counter.vue -->
<script setup>
const props = defineProps({ count: { type: Number, required: true } });
const emit = defineEmits(['update']);
</script>
<template>
<button @click="emit('update', props.count + 1)">{{ props.count }}</button>
</template>
<!-- Батьківський компонент -->
<Counter :count="clicks" @update="clicks = $event" />
Головне правило: дочірній компонент не змінює props. Значення належить батькові: якщо дитина його змінить, батько при наступному рендері перезапише зміну, а Vue виведе попередження. Дитина просить зміну подією, а батько вирішує.
v-model на компоненті - скорочення для пари «prop + подія»:
<SearchInput v-model="query" />
<!-- те саме, що :modelValue="query" @update:modelValue="query = $event" -->
З Vue 3.4 у дочірньому компоненті це зручно оформлюється через defineModel().
Коли props і emits незручні: якщо дані треба передати через п'ять рівнів компонентів, які самі їх не використовують. Тоді - provide/inject або стор (Pinia).
Докладніше в документації: Props: односпрямований потік даних