How to Use Vue Emit for Sibling Component Communication in Vue 3
To communicate between sibling components using vue emit, emit an event from Child A to the parent component, which then updates reactive state or calls a method on Child B, acting as the mediator between siblings.
In Vue 3, the emit function is scoped to the parent-child relationship, meaning sibling components cannot directly listen to each other's events. According to the vuejs/core source code, the runtime validates and processes these events through specific internal modules that enforce unidirectional data flow while maintaining type safety.
Why Vue Emit Requires a Parent Mediator for Siblings
Vue's event system is intentionally designed so that events bubble upward only to the immediate parent, not sideways to siblings. When a child component calls emit('my-event', payload), the runtime checks the declaration in packages/runtime-core/src/componentEmits.ts (lines 121-148) to validate the event name against the component's emits option. The parent receives this event via an onXxx listener, which packages/runtime-core/src/componentProps.ts (line 270) distinguishes from standard props.
This architecture ensures that siblings remain decoupled. Child A never imports Child B or vice versa; both depend only on the parent contract, making components reusable and testable.
Core Implementation Files for Vue Emit
Understanding how vue emit works internally helps debug event propagation issues. The vuejs/core repository handles emit logic across these key files:
| File | Purpose |
|---|---|
packages/runtime-core/src/componentEmits.ts |
Normalizes emits options, validates emitted event names at runtime, and caches emit listeners. |
packages/runtime-core/src/componentProps.ts |
Detects onXxx props that correspond to declared emits, preventing prop-to-event collisions. |
packages/runtime-core/src/apiSetupHelpers.ts |
Implements defineEmits (lines 110-218) for type-safe emit functions in <script setup> components. |
packages/runtime-core/src/componentRenderUtils.ts |
Emits warnings when undeclared events are triggered, helping catch misuse during development. |
Practical Patterns for Sibling Communication with Vue Emit
Pattern 1: Parent-Mediated State Updates (Options API)
The classic approach uses the parent as a reactive store. Child A emits an event; the parent updates data bound to Child B via props.
<!-- Parent.vue -->
<template>
<ChildA @update="onChildAUpdate" />
<ChildB :shared="shared" />
</template>
<script>
import ChildA from './ChildA.vue'
import ChildB from './ChildB.vue'
export default {
components: { ChildA, ChildB },
data() {
return { shared: null }
},
methods: {
onChildAUpdate(value) {
this.shared = value
}
}
}
</script>
<!-- ChildA.vue -->
<template>
<button @click="notify">Send</button>
</template>
<script>
export default {
emits: ['update'],
methods: {
notify() {
this.$emit('update', 'hello from A')
}
}
}
</script>
<!-- ChildB.vue -->
<template>
<p>Message: {{ shared }}</p>
</template>
<script>
export default {
props: { shared: String }
}
</script>
As implemented in componentEmits.ts, the emits: ['update'] declaration ensures Vue validates the event name at runtime, preventing typos in the template.
Pattern 2: Type-Safe Emits with defineEmits (Composition API)
For <script setup> components, use defineEmits to get full TypeScript inference. The helper is defined in packages/runtime-core/src/apiSetupHelpers.ts (lines 110-218).
<!-- Parent.vue -->
<template>
<ChildA @msg="handleMsg" />
<ChildB :msg="msgFromA" />
</template>
<script setup>
import { ref } from 'vue'
import ChildA from './ChildA.vue'
import ChildB from './ChildB.vue'
const msgFromA = ref('')
function handleMsg(payload) {
msgFromA.value = payload
}
</script>
<!-- ChildA.vue -->
<template>
<button @click="send">Emit</button>
</template>
<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['msg'])
function send() {
emit('msg', 'payload from A')
}
</script>
<!-- ChildB.vue -->
<template>
<div>{{ msg }}</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
msg: String
})
</script>
Pattern 3: Direct Method Invocation via Template Refs
For imperative operations, the parent can hold a template ref to Child B and call its exposed methods after receiving the emit from Child A.
<!-- Parent.vue -->
<template>
<ChildA @request="onRequest" />
<ChildB ref="childBRef" />
</template>
<script setup>
import { ref } from 'vue'
import ChildA from './ChildA.vue'
import ChildB from './ChildB.vue'
const childBRef = ref(null)
function onRequest(payload) {
childBRef.value?.receive(payload)
}
</script>
<!-- ChildA.vue -->
<script setup>
const emit = defineEmits(['request'])
function ask() { emit('request', 'data') }
</script>
<!-- ChildB.vue -->
<script setup>
function receive(data) {
console.log('Received from sibling via parent:', data)
}
defineExpose({ receive })
</script>
Even with direct method calls, the parent remains the sole mediator—Child A never imports Child B, maintaining the decoupled architecture that Vue's emit system enforces.
Validation and Runtime Safety
Vue's runtime provides safeguards to prevent emit misuse. When you declare emits: ['eventName'], packages/runtime-core/src/componentEmits.ts validates the event name against this whitelist. If you emit an undeclared event, componentRenderUtils.ts generates a development warning.
Additionally, componentProps.ts (line 270) distinguishes between event listeners (onXxx props) and standard props, ensuring that v-on listeners don't collide with props of the same name. This separation is crucial when the parent forwards data to siblings after receiving an emit.
Summary
- Vue emit is parent-scoped: Events bubble up only to the immediate parent, never sideways to siblings.
- Use the parent as mediator: Child A emits to the parent; the parent updates state or calls methods on Child B.
- Declare emits explicitly: Use the
emitsoption ordefineEmitsto enable runtime validation and type safety as implemented incomponentEmits.tsandapiSetupHelpers.ts. - Maintain decoupling: Siblings should never import each other; all communication flows through the parent mediator.
- Leverage template refs sparingly: For imperative sibling communication, expose methods via
defineExposeand invoke them through parent-held refs.
Frequently Asked Questions
Can sibling components communicate directly without a parent?
No. According to the vuejs/core source code, the emit function in componentEmits.ts is designed to dispatch events only to the parent component's listeners. Siblings have no direct event bus connection. Any direct communication would require importing one sibling into another, which violates Vue's component decoupling principles and makes testing difficult.
How does defineEmits improve type safety for sibling communication?
defineEmits, implemented in packages/runtime-core/src/apiSetupHelpers.ts (lines 110-218), generates a typed emit function based on TypeScript declarations. When you write const emit = defineEmits<{ (e: 'msg', value: string): void }>(), the compiler ensures you can only emit 'msg' with a string payload. This prevents runtime errors where the parent handler receives unexpected data types before forwarding to the sibling.
What happens if I emit an event that isn't declared in the emits option?
Vue's runtime validator in componentEmits.ts checks emitted events against the component's emits declaration. If you emit an undeclared event in development mode, componentRenderUtils.ts emits a console warning indicating that the event is not declared. This helps catch typos and ensures that parent components only listen for intentionally exposed events, maintaining clear contracts between siblings and their mediator parent.
Is using template refs for sibling communication considered an anti-pattern?
Using template refs to call methods on sibling components is not strictly an anti-pattern, but it should be used sparingly. While the parent still mediates the communication (preserving the unidirectional flow), imperative method calls via defineExpose bypass Vue's reactive prop system. This approach is appropriate for discrete actions like "scroll to item" or "focus input," but for data flow, prefer reactive props and events as validated by componentEmits.ts and componentProps.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →