Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як ефективно передавати дані між компонентми?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Props та emit** - механізм Vue для комунікації між компонентами: props передають дані від батька до дочірнього (read-only), emit надсилає події від дочірнього до батька. ```vue const emit = defineEmits(['update']); emit('update', newValue); // дочірній надсилає подію ``` **Головне:** дані течуть вниз через props, події вгору через emit. Для сусідніх компонентів потрібен Pinia.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Props та emit** - вбудований механізм Vue для комунікації між компонентами: props передають дані від батька до дочірнього, emit надсилає події назад від дочірнього до батька. ## Теорія ### TL;DR - Props = дані течуть вниз, read-only у дочірньому. Emit = події течуть вгору, батько їх обробляє. - Props як пошта від головного офісу до філій; emit - зворотній конверт із відповіддю. - Напряму мутувати props у дочірньому не можна: Vue попередить, і зміна не дійде до батька. - Більше 2 рівнів глибини або сусідні компоненти? Pinia, а не ланцюжок props. - `v-model` - скорочення для props + emit: `:modelValue` вниз, `update:modelValue` вгору. ### Швидкий приклад ```vue <!-- Child.vue --> <script setup> const props = defineProps(['count']); const emit = defineEmits(['update']); const increment = () => emit('update', props.count + 1); </script> <template> <button @click="increment">{{ props.count }}</button> </template> ``` ```vue <!-- Parent.vue --> <script setup> import { ref } from 'vue'; const count = ref(0); const handleUpdate = (newCount) => { count.value = newCount; }; </script> <template> <Child :count="count" @update="handleUpdate" /> </template> ``` Дочірній показує значення батька. Клік запускає `emit('update', newValue)`. Батько оновлює свій `ref`. Дочірній перерендерюється з новим значенням. Це і є весь патерн. ### Головна різниця Props створюють однонаправлений реактивний зв'язок від батька до дочірнього. Коли значення у батька змінюється, дочірній оновлюється автоматично. Але дочірній не може записувати назад: props read-only всередині компонента. Це робить потік даних передбачуваним. Emit йде у зворотному напрямку. Дочірній запускає іменовану подію з payload. Батько слухає і вирішує, що з цим робити. Батько завжди контролює свій власний стан; дочірній лише сповіщає. ### Коли що використовувати - Передача конфігурації або даних для відображення вниз (тема, список задач, профіль) → `props` - Дія у дочірньому має щось змінити у батьку (відправка форми, кнопка видалення, закриття модалки) → `emit` - Двостороння синхронізація, як кастомне поле вводу → `v-model` (`:modelValue` + `update:modelValue`) - Дані треба передати через кілька рівнів дерева компонентів → `provide/inject` - Сусідні компоненти або глобальний стан застосунку → Pinia Практичне правило: якщо props передається через компонент, який їх не використовує, просто щоб донести дані глибше, це prop drilling. Час брати Pinia. ### Таблиця порівняння | Аспект | Props | Emit | |---|---|---| | Напрямок | Батько до дочірнього | Дочірній до батька | | Мутабельність | Read-only у дочірньому | Запускає обробник батька | | Реактивність | Автоматично при зміні батька | Вручну через payload події | | Оголошення | `defineProps(['name'])` | `defineEmits(['event-name'])` | | Коли використовувати | Конфіг, дані для відображення | Дії, колбеки, результати | ### Як Vue обробляє це всередині Компілятор Vue перетворює `<Child :count="count" @update="handleUpdate" />` на виклик `createVNode(Child, { count: count, onUpdate: handleUpdate })`. Props стають реактивними геттерами на proxy-об'єкті дочірнього компонента. Будь-яка зміна у батька автоматично тригерить ефект рендерингу дочірнього. Emit працює інакше. Він реєструється через `defineEmits` і диспатчиться під час patch-циклу дочірнього. Обробник батька - це просто властивість `onUpdate` у vnode, не справжня DOM-подія браузера. Особисто я найчастіше бачу проблему з async emit на першому ж Nuxt-проєкті: коли emit викликається всередині `async`-функції, payload - це звичайний JavaScript-об'єкт, а не реактивне значення. Батько сам має присвоїти його до `ref`. ### Типові помилки **Пряма мутація props:** ```vue <script setup> const props = defineProps(['count']); // Неправильно: Vue попередить, зміна не дійде до батька props.count++; </script> ``` Props у Vue 3 - це readonly proxy. Запис у них не оновлює батька і кидає runtime-попередження. Замість цього надсилай подію через emit. **Пропуск оголошень у `defineEmits`:** ```vue <script setup> const emit = defineEmits(['update']); // Неправильно: 'typo-event' не оголошено, ігнорується без помилки emit('typo-event', data); </script> ``` Без оголошення події в `defineEmits` помилка у назві події непомітна. Оголошуй усі події, які використовуєш. У Vue 3.3+ можна додати валідаційну функцію для перевірки payload. **Функція як prop замість emit:** ```vue <!-- Неправильно --> <Child :on-submit="handleSubmit" /> ``` Технічно працює, якщо дочірній викликає `props.onSubmit()` напряму. Але це ламає однонаправлену модель подій і робить API компонента незрозумілим. Правильно: `@submit="handleSubmit"`. **Мутація вкладеного об'єкта в props:** ```vue <!-- Небезпечно --> <Child :task="task" /> <!-- Всередині Child: props.task.title = 'new' також змінює об'єкт батька --> ``` Об'єкти передаються за посиланням. Зміна вкладеного поля у дочірньому мовчки мутує дані батька. Огортай prop у `readonly()` або передавай копію: `v-bind="{ ...task }"`. ### Де використовується - **TodoMVC (офіційний приклад Vue):** props передають задачі до рядків списку; emit обробляє toggle і delete назад до батька. - **Element Plus форми:** кожен input використовує `v-model`, який компілюється в `:modelValue` + `emit('update:modelValue')`. - **Nuxt UI модалки:** props передають початкові дані форми; `emit('close', result)` повертає результат. - **Quasar QDialog:** налаштовується через props, відправляє дані форми через `emit('ok', formData)`. - **Pinia у великих застосунках:** коли Header і Cart обидва потребують даних користувача, Pinia замінює 3+ рівні prop drilling. ### Follow-up питання **Q:** Що таке prop drilling і як його уникнути? **A:** Передача props через проміжні компоненти, які їх не використовують, щоб донести дані глибше. Уникай через `provide/inject` для статичних залежностей або Pinia для реактивного спільного стану. **Q:** Як `v-model` працює всередині у Vue 3? **A:** Компілюється в `:modelValue="value"` prop та `@update:modelValue="value = $event"` emit. На одному компоненті можна мати кілька `v-model` з іменованими аргументами: `v-model:title="form.title"`. **Q:** Яка різниця між `$emit` та `defineEmits`? **A:** `defineEmits` - синтаксис `<script setup>`, оголошує події і генерує TypeScript-типи. `$emit` - runtime-метод з Options API. В `<script setup>` виклик `const emit = defineEmits([...])` дає ту саму функцію. **Q:** Що станеться, якщо дочірній emit-не до того, як батько підключив слухача? **A:** Подія загубиться. Vue не ставить emit у чергу. Якщо є проблеми з тимінгом, використовуй `nextTick`. **Q:** Чи можуть часті emit, наприклад 100 на секунду, впливати на продуктивність? **A:** Сам dispatch дешевий, O(1). Проблема в тому, що батько робить у обробнику. Якщо кожен emit тригерить великий ре-рендер, це накопичується. Батч через `nextTick`, профілюй у flamegraph Vue DevTools. ## Приклади ### Базовий: лічильник з дочірньою кнопкою ```vue <!-- CounterButton.vue --> <script setup> const props = defineProps({ count: Number }); const emit = defineEmits(['increment']); </script> <template> <button @click="emit('increment')"> Натиснуто {{ props.count }} разів </button> </template> ``` ```vue <!-- App.vue --> <script setup> import { ref } from 'vue'; import CounterButton from './CounterButton.vue'; const clicks = ref(0); </script> <template> <CounterButton :count="clicks" @increment="clicks++" /> </template> ``` Стан живе у батьку. Дочірній - чистий компонент відображення, який тригерить події. Це базовий патерн для майже кожного UI-компонента у Vue. ### Середній: форма задачі з emit до батька ```vue <!-- TaskForm.vue --> <script setup> import { ref } from 'vue'; const emit = defineEmits(['add-task']); const title = ref(''); const submit = () => { if (!title.value.trim()) return; emit('add-task', { id: Date.now(), title: title.value, done: false }); title.value = ''; // Скидаємо локальний стан після emit }; </script> <template> <form @submit.prevent="submit"> <input v-model="title" placeholder="Нова задача" /> <button type="submit">Додати</button> </form> </template> ``` ```vue <!-- TaskList.vue --> <script setup> import { ref } from 'vue'; import TaskForm from './TaskForm.vue'; const tasks = ref([]); </script> <template> <TaskForm @add-task="(task) => tasks.push(task)" /> <ul> <li v-for="task in tasks" :key="task.id">{{ task.title }}</li> </ul> </template> ``` Форма зберігає локальний стан поля вводу. `TaskList` зберігає масив задач. Emit - міст між ними. Такий самий підхід використовується в TodoMVC, офіційному демо-застосунку Vue. ### Просунутий: async emit і реактивність ```vue <!-- DataLoader.vue --> <script setup> const emit = defineEmits(['data-loaded']); const load = async () => { const data = await fetch('/api/tasks').then(r => r.json()); // data - звичайний JS-масив, не реактивний emit('data-loaded', data); }; </script> <template> <button @click="load">Завантажити задачі</button> </template> ``` ```vue <!-- Parent.vue --> <script setup> import { ref } from 'vue'; import DataLoader from './DataLoader.vue'; const tasks = ref([]); const onDataLoaded = (data) => { tasks.value = data; // Присвоєння до ref повертає реактивність }; </script> <template> <DataLoader @data-loaded="onDataLoaded" /> <ul> <li v-for="task in tasks" :key="task.id">{{ task.title }}</li> </ul> </template> ``` Payload emit - завжди звичайне JavaScript-значення. Реактивна обгортка через подію не передається. Батько сам відповідає за те, щоб покласти дані у `ref` або `reactive`. Розробники, які очікують, що emit автоматично зберігає реактивність, отримають список, який відмовляється оновлюватися. Не зберігає.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.