Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «defineProps, defineEmits та defineExpose у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)`defineProps`, `defineEmits` та `defineExpose` - макроси компілятора Vue 3, доступні у `<script setup>` без імпортів. `defineProps` оголошує пропси з TypeScript-типами, `defineEmits` оголошує події які компонент може надсилати, а `defineExpose` контролює що батько бачить через template ref. ```ts const props = defineProps<{ count: number }>() const emit = defineEmits<{ (e: 'update:count', value: number): void }>() defineExpose({ reset }) ``` **Ключове:** У `<script setup>` нічого не є публічним за замовчуванням. `defineExpose` - єдиний спосіб відкрити компонент для батьківського ref.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення`defineProps`, `defineEmits` та `defineExpose` - це **макроси компілятора** у `<script setup>` Vue 3. Вони оголошують що компонент приймає, що він сигналізує назовні, і що батько може викликати ззовні. Без жодних імпортів. ## Теорія ### TL;DR - Уяви компонент як квартиру: `defineProps` - вхідні двері (що входить), `defineEmits` - дзвоник (що сигналить назовні), `defineExpose` відчиняє конкретне вікно (до чого сусід може дотягтись) - Головна відмінність від Options API: макроси дають TypeScript-інференс автоматично, без ручного об'єкта `props: {}` - `defineProps` - коли батько передає дані. `defineEmits` - для будь-якої події або v-model. `defineExpose` - тільки коли батькові треба викликати метод дитини напряму - У `<script setup>` нічого не є публічним за замовчуванням, тому `defineExpose` - виняток, а не правило ### Швидкий приклад ```vue <script setup lang="ts"> // Типізовані пропси - без import const props = defineProps<{ title: string; count: number }>() // Типізовані події const emit = defineEmits<{ (e: 'update:count', value: number): void }>() const localCount = ref(props.count) function increment() { localCount.value++ emit('update:count', localCount.value) // сигнал батьку } // Відкриваємо тільки те, що батько реально потребує defineExpose({ increment }) </script> ``` Батько викликає `childRef.value.increment()` через template ref. Все інше залишається приватним. ### Ключова відмінність від Options API Options API оголошує `props`, `emits` та `expose` як звичайні об'єкти всередині `defineComponent`. Макроси роблять те саме під час збірки, але TypeScript-плагін Vue підхоплює generic-типи і дає повний інференс у шаблоні та батьківських компонентах. Різниці у продуктивності між двома підходами немає. ### Коли що використовувати - Батько передає дані → `defineProps` (завжди першим у `<script setup>`) - Дитина повідомляє батька або підтримує v-model → `defineEmits` - Батько хоче викликати метод дитини напряму → `defineExpose` (рідко; для потоку даних краще використовуй події) - Дефолтні значення для опціональних типізованих пропсів → `withDefaults(defineProps<...>(), { count: 0 })` ### Як компілятор це обробляє SFC-плагін Vite перетворює макроси під час збірки. `defineProps()` стає валідованим прив'язуванням `__props`; `defineEmits()` реєструє валідатори подій в опціях компонента; `defineExpose()` встановлює `exposeProxy` на екземплярі, тому батько через `ref.value` бачить тільки те, що в списку. У браузер не потрапляє жоден виклик функції `defineProps` - він повністю зникає після компіляції. ### Типові помилки **1. Масив рядків замість типів** ```ts // Неправильно - TypeScript не знає типів, складні структури стають `any` defineProps(['title', 'count']) // Правильно defineProps<{ title: string; count: number }>() ``` **2. Масив або об'єкт як дефолт-літерал, а не фабрика** ```ts // Неправильно - всі екземпляри ділять одне посилання на масив withDefaults(defineProps<{ items?: string[] }>(), { items: [] }) // Правильно withDefaults(defineProps<{ items?: string[] }>(), { items: () => [] }) ``` Кожен екземпляр отримує свій масив. Та сама проблема була в `data` у Vue 2, і досі ловить людей зненацька. **3. Пряма мутація пропсу** ```ts // Неправильно - Vue попереджає в dev-режимі, реактивність пропускає оновлення props.config.theme = 'dark' // Правильно - емітуй зміну emit('update:config', { ...props.config, theme: 'dark' }) ``` Пропси доступні тільки для читання. Мутація вкладених об'єктів обходить систему реактивності і викликає попередження Vue в режимі розробки. **4. Відкриття реактивного стану замість методів** ```ts // Ризиковано - батько може читати І писати внутрішній стан defineExpose({ count, data }) // Краще - батько може тільки запускати дії defineExpose({ increment, reset }) ``` Якщо відкрити ref, батько може його мутувати. Це ламає інкапсуляцію і ускладнює пошук багів. **5. Забути `withDefaults` для опціональних типізованих пропсів** ```ts // Неправильно - опціональний пропс буде `undefined`, якщо не передати defineProps<{ count?: number }>() // props.count → undefined, не 0 // Правильно withDefaults(defineProps<{ count?: number }>(), { count: 0 }) ``` ### Де зустрічається в реальних проектах - **Element Plus**: `defineEmits` для подій рядків у `ElTable` (`update:selection`) - **Nuxt Content**: `defineProps` для пропсів документів у компонентах на основі Markdown - **Модальні вікна**: `defineExpose({ open, close })` щоб батько міг керувати модалкою напряму - **Vitest**: мокування подій через `vi.fn()` у тестах компонента ### Питання на співбесіді **Q:** Яка TypeScript-перевага `defineProps<{ foo: string }>()`? **A:** Generic-форма дає точні типи для `props.foo` у шаблоні та у функції setup. Оголошення через масив рядків залишає все як `any`. **Q:** Як `defineEmits` валідує події? **A:** На рівні TypeScript-плагіна перевіряється що `emit('назва', payload)` відповідає оголошеній сигнатурі. Під час виконання Vue перевіряє назви подій і попереджає, якщо щось незадекларовано. **Q:** Коли `defineExpose` реально потрібен? **A:** Тільки коли батько тримає template ref і хоче викликати метод або прочитати значення напряму. У `<script setup>` екземпляр компонента закритий за замовчуванням - без `defineExpose` батько не побачить нічого через `ref.value`. **Q:** Що `defineModel` (Vue 3.4+) замінює? **A:** Він автоматично генерує `defineProps({ modelValue })` плюс `defineEmits(['update:modelValue'])`. Замість двох макросів для v-model пропсу пишеш `const model = defineModel<string>()` і прив'язуєш `v-model="model"` у шаблоні. ## Приклади ### Базовий: кнопка з лічильником і v-model ```vue <template> <button @click="increment">{{ props.count }}</button> </template> <script setup lang="ts"> const props = defineProps<{ count: number }>() const emit = defineEmits<{ (e: 'update:count', value: number): void }>() function increment() { emit('update:count', props.count + 1) } </script> ``` Батько використовує `<CountButton v-model:count="total" />`. Дитина не власник лічильника - тільки сигналізує що він має змінитись. ### Середній рівень: редактор контенту з кількома подіями ```vue <template> <div class="editor"> <textarea v-model="content" /> <button @click="publish">Опублікувати</button> </div> </template> <script setup lang="ts"> interface Post { id: string; content: string; published: boolean } const props = withDefaults( defineProps<{ post: Post }>(), { post: () => ({ id: '', content: '', published: false }) } ) const emit = defineEmits<{ (e: 'update:post', post: Post): void (e: 'publish', id: string): void }>() const content = ref(props.post.content) watch(content, (val) => emit('update:post', { ...props.post, content: val })) function publish() { emit('publish', props.post.id) } </script> ``` Компонент тримає локальний стан textarea, але кожну зміну передає батьку. Цей патерн зустрічається в редакторах на основі Nuxt Content. ### Просунутий рівень: модальне вікно з `defineExpose` ```vue <!-- Modal.vue --> <template> <div v-if="visible" class="modal"> <slot /> <button @click="close">Закрити</button> </div> </template> <script setup lang="ts"> const visible = ref(false) function open() { visible.value = true } function close() { visible.value = false } // Відкриваємо тільки методи, а не сам ref `visible` defineExpose({ open, close }) </script> ``` ```vue <!-- Parent.vue --> <script setup lang="ts"> import Modal from './Modal.vue' const modal = ref<InstanceType<typeof Modal> | null>(null) function showModal() { modal.value?.open() // типобезпечно завдяки defineExpose } </script> <template> <Modal ref="modal"> <p>Вміст тут</p> </Modal> <button @click="showModal">Відкрити</button> </template> ``` Батько може викликати `open` і `close`, але не може читати чи записувати `visible` напряму. Саме для цього відкривають методи, а не стан.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.