Suggest an editImprove this articleRefine the answer for “How to efficiently pass data between components?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Props and emit** - Vue's core pattern for component communication: props pass data from parent to child (read-only in child), emit sends events from child back to parent. ```vue const emit = defineEmits(['update']); emit('update', newValue); // child fires event up ``` **Key:** data flows down via props, events flow up via emit. Siblings or deep trees need Pinia.Shown above the full answer for quick recall.Answer (EN)Image**Props and emit** - Vue's built-in system for parent-child communication: props carry data down from parent to child, emit sends events back up from child to parent. ## Theory ### TL;DR - Props = data flowing down, read-only in the child. Emit = events flowing up, handled by the parent. - Think of props as a config the branch office receives from HQ. Emit is the reply envelope sent back. - Mutating props directly in a child breaks reactivity and triggers Vue warnings. Always emit instead. - More than 2 levels deep, or sibling components? Use Pinia rather than chaining props. - `v-model` is shorthand for props + emit combined: `:modelValue` down, `update:modelValue` up. ### Quick Example ```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> ``` Child displays the parent's count. Click fires `emit('update', newValue)`. Parent handler updates its `ref`. Child re-renders with the new value. That cycle is the whole pattern. ### Key Difference Props create a one-way reactive link from parent to child. When the parent's value changes, the child updates automatically. But the child cannot write back: props are read-only inside the child. This keeps data flow predictable and makes debugging straightforward. Emit goes the other direction. The child fires a named event with a payload. The parent listens and decides what to do with it. The parent always controls its own state; the child only notifies. ### When to Use - Passing config or display data downward (theme, items list, user profile) → `props` - Child action needs to change something in the parent (form submit, delete button, modal close) → `emit` - Two-way sync like a custom input field → `v-model` (`:modelValue` prop + `update:modelValue` emit) - Data needs to skip several levels of the component tree → `provide/inject` - Sibling components or global app state → Pinia store The practical rule: if you find yourself passing a prop through a component that does not actually use it, just to get the data deeper, that is prop drilling. At that point, reach for Pinia. ### Comparison Table | Aspect | Props | Emit | |---|---|---| | Direction | Parent to child | Child to parent | | Mutability | Read-only in child | Triggers parent handler | | Reactivity | Automatic on parent change | Manual via event payload | | Declaration | `defineProps(['name'])` | `defineEmits(['event-name'])` | | When to use | Config, display data, lists | Actions, callbacks, results | ### How Vue Handles This Internally Vue's compiler transforms `<Child :count="count" @update="handleUpdate" />` into `createVNode(Child, { count: count, onUpdate: handleUpdate })`. Props become reactive getters on the child's proxy object. Any change in the parent triggers the child's render effect automatically. Emits work differently. They are registered via `defineEmits` and dispatched during the child's patch cycle. The parent listener is just the `onUpdate` property on the vnode, not a real browser DOM event. One thing I see trip up developers on their first async-heavy project: when you emit from inside an `async` function, the payload is a plain JavaScript object, not a reactive value. The parent must assign it to a `ref` to get reactivity back. It does not happen automatically. ### Common Mistakes **Mutating a prop directly:** ```vue <script setup> const props = defineProps(['count']); // Wrong: Vue warns, nothing propagates to parent props.count++; </script> ``` Props are readonly proxies in Vue 3. Writing to them does not update the parent and throws a runtime warning. Emit the change instead and let the parent handle it. **Skipping `defineEmits` declarations:** ```vue <script setup> const emit = defineEmits(['update']); // Wrong: 'typo-event' not declared, fires silently with no error emit('typo-event', data); </script> ``` Without declaring an event in `defineEmits`, typos in event names pass silently. Add every event name you use. In Vue 3.3+ you can attach a validation function per event to catch bad payloads before they reach the parent. **Passing a function as a prop for callbacks:** ```vue <!-- Wrong --> <Child :on-submit="handleSubmit" /> ``` This technically works if the child calls `props.onSubmit()` directly, but it breaks the unidirectional event model and makes the component API harder to read. Use event binding instead: `@submit="handleSubmit"`. **Mutating nested object props:** ```vue <!-- Tricky --> <Child :task="task" /> <!-- Inside Child: props.task.title = 'new' also changes the parent's object --> ``` Objects pass by reference. A child modifying a nested property silently changes the parent's data. Wrap the prop with `readonly()` inside the child, or pass a spread copy: `v-bind="{ ...task }"`. ### Real-World Usage - **TodoMVC (official Vue example):** props carry task items down to list rows; emit handles toggle and delete back up to the parent. - **Element Plus form inputs:** every input uses `v-model`, which compiles to `:modelValue` + `emit('update:modelValue')`. - **Nuxt UI modals:** props supply initial form data; `emit('close', result)` returns the result to the caller. - **Quasar QDialog:** configured via props, submits form data with `emit('ok', formData)`. - **Pinia in larger apps:** when Header and Cart both need user data, a Pinia store replaces 3+ levels of prop drilling. ### Follow-Up Questions **Q:** What is prop drilling and how do you avoid it? **A:** Passing props through intermediate components that do not use them, just to get data deeper in the tree. Avoid it with `provide/inject` for static dependencies, or Pinia for reactive shared state. **Q:** How does `v-model` work under the hood in Vue 3? **A:** It compiles to `:modelValue="value"` prop and `@update:modelValue="value = $event"` emit. You can have multiple `v-model` bindings on a single component using named arguments like `v-model:title="form.title"`. **Q:** What is the difference between `$emit` and `defineEmits`? **A:** `defineEmits` is the `<script setup>` syntax; it declares events and generates TypeScript types. `$emit` is the runtime method available in Options API. In `<script setup>`, calling `const emit = defineEmits([...])` gives you the same callable function. **Q:** What happens if a child emits before the parent listener is ready? **A:** The event is lost. Vue does not queue emits waiting for listeners. If timing matters, use `nextTick` or confirm the parent is mounted before the child fires. **Q:** Can frequent emits, say 100 per second, cause performance problems? **A:** The dispatch itself is cheap, O(1). The real bottleneck is what the parent does inside the handler. If every emit triggers a large re-render, that adds up. Batch updates with `nextTick` and profile with Vue DevTools flamegraph to find the actual cost. ## Examples ### Basic: Counter with child button ```vue <!-- CounterButton.vue --> <script setup> const props = defineProps({ count: Number }); const emit = defineEmits(['increment']); </script> <template> <button @click="emit('increment')"> Clicked {{ props.count }} times </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> ``` State lives in the parent. The child is a pure display component that fires events. This is the base pattern for almost every Vue UI component. ### Intermediate: Task form that emits to parent list ```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 = ''; // Reset local state after emit }; </script> <template> <form @submit.prevent="submit"> <input v-model="title" placeholder="New task" /> <button type="submit">Add</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> ``` The form owns its local `title` state. `TaskList` owns the array. Emit is the bridge between them. This is the same architecture used in TodoMVC, the official Vue demo app. ### Advanced: Async emit and the reactivity gap ```vue <!-- DataLoader.vue --> <script setup> const emit = defineEmits(['data-loaded']); const load = async () => { const data = await fetch('/api/tasks').then(r => r.json()); // data is a plain JS array, not reactive emit('data-loaded', data); }; </script> <template> <button @click="load">Load tasks</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; // Assigning to ref restores reactivity }; </script> <template> <DataLoader @data-loaded="onDataLoaded" /> <ul> <li v-for="task in tasks" :key="task.id">{{ task.title }}</li> </ul> </template> ``` Emitted payloads are always plain JavaScript values. No reactive wrapper survives the event boundary. The parent is responsible for placing the data into a `ref` or `reactive`. Developers who expect emitted data to stay reactive automatically will get a list that refuses to update. It does not stay reactive.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.