Props, события и v-model
Данные текут вниз через props, события всплывают вверх через emit — это однонаправленный поток, на котором держится предсказуемость Vue.
Суть: родитель передаёт данные дочернему компоненту через props (объявляются defineProps). Ребёнок сообщает родителю о событиях через emit (объявляются defineEmits). v-model связывает значение в обе стороны как синтаксический сахар над props и событием.
Компоненты — это кирпичики, и им надо как-то общаться. Vue (а значит и Nuxt) строит общение на строгом принципе: данные идут вниз, события — вверх. Родитель передаёт ребёнку значения через props. Ребёнок не меняет их напрямую, а «кричит» родителю через событие, что хочет изменения. Эта однонаправленность делает поток данных предсказуемым.
Props объявляются через defineProps. В дочернем компоненте:
<script setup>
const props = defineProps({
title: String,
price: Number,
})
</script>
<template>
<h2>{{ title }} — {{ price }} ₽</h2>
</template>
Родитель передаёт значения как атрибуты: <ProductCard title="Книга" :price="500" />. Обратите внимание на двоеточие перед price: оно означает «это выражение JS (число)», а не строка.
Чтобы ребёнок сообщил о действии, он объявляет события через defineEmits и вызывает emit:
<script setup>
const emit = defineEmits(["add-to-cart"])
function buy() {
emit("add-to-cart", props.title) // сигнал наверх
}
</script>
Родитель ловит событие: <ProductCard @add-to-cart="onAdd" />. А v-model — это удобный сахар, объединяющий props и событие update:modelValue в двустороннюю связь, что особенно удобно для полей ввода.
Поток данных между компонентами Родитель --props (вниз)--> Ребёнок Родитель <--emit (вверх)-- Ребёнок v-model = props (modelValue) + emit (update:modelValue)
Стоит проговорить и обратную сторону авто-импортов — отладку. Когда импорт не виден в коде, новичку бывает трудно понять, откуда взялась функция и где её определение. Здесь выручают типы: Nuxt генерирует декларации для всех авто-импортов, и редактор с поддержкой TypeScript подскажет источник по переходу к определению. Если же имя не разрешается, чаще всего дело в том, что файл лежит не в сканируемой папке или назван не по соглашению. Поэтому держите composables строго в composables/, а компоненты — в components/: предсказуемое расположение и есть цена за магию авто-импорта.
Как работает под капотом
Props реактивны: когда родитель меняет переданное значение, Vue обновляет дочерний компонент. Но ребёнок не должен мутировать props напрямую — это нарушит однонаправленность и вызовет предупреждение. Вместо этого он эмитит событие, родитель меняет своё состояние, и обновление приходит обратно через props. v-model автоматизирует именно эту пару «значение + событие обновления».
Смоделируем поток props/emit без Vue — на чистых функциях и колбэке:
// Родитель держит состояние, ребёнок просит изменения через emit.
function createChild(props, emit) {
return {
render: () => "[" + props.title + ": " + props.count + "]",
buy: () => emit("add", props.title), // сигнал наверх
};
}
// Родитель
let cart = [];
const props = { title: "Книга", count: 3 };
const child = createChild(props, (event, payload) => {
if (event === "add") cart.push(payload); // реагируем на событие
});
console.log("Ребёнок рисует:", child.render());
child.buy(); // ребёнок эмитит "add"
child.buy();
console.log("Корзина родителя:", cart);
Попробуй сам ▶ — ребёнок не трогает корзину сам, он лишь эмитит событие, а решение принимает родитель. Это и есть «вниз props, вверх события».
Частые ошибки
- Мутировать props внутри ребёнка. Vue это запрещает. Эмитьте событие и меняйте источник у родителя.
- Забыть двоеточие.
:price="500"— число;price="500"— строка"500". - Передавать всё через события, минуя props. Для входных данных props проще и нагляднее.
Best practices
- Держите props «только для чтения» — это контракт, а не изменяемое состояние ребёнка.
- Именуйте события глаголами действия:
add-to-cart,delete,submit. - Для полей ввода используйте
v-modelвместо ручной пары props+emit.
Итог: props несут данные вниз, события — вверх, а v-model аккуратно связывает их в двустороннюю привязку. Этот контракт делает дерево компонентов предсказуемым. Дальше — куда выносить общую логику: composables.