Custom Events and Component Communication in Vue - `emit`, `provide/inject`, and When to Use a Store
Vue gives you a few powerful ways to handle communication between components. But which one to choose - and when?
Let’s break it down with practical examples and rules of thumb that I follow in my own projects.
1. emit - Direct Parent-Child Communication
The most common and preferred way to pass events from a child to its parent is using emit.
Example
<!-- ChildComponent.vue -->
<template>
<button @click="$emit('increment')">+1</button>
</template>
<script setup lang="ts">
defineEmits(['increment'])
</script>
<!-- Parent.vue -->
<template>
<ChildComponent @increment="count++" />
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
let count = $ref(0)
</script>
Use emit when:
-
The component is used directly by the parent
-
The event is simple (like “clicked”, “submitted”, “closed”)
2. provide/inject - Dependency Injection for Distant Components
This allows deeply nested components to access shared values without having to pass props through every intermediate layer.
Example
// Parent.vue
provide('theme', 'dark')
// Any deeply nested component
const theme = inject('theme')
Use provide/inject when:
-
You want to avoid prop drilling
-
You’re sharing context (like theme, user, or layout data)
-
You don’t need reactivity across the whole app
Downsides:
-
Not reactive by default
-
Harder to track dependencies than props or store
3. Store (e.g. Pinia) - Global and Reactive State
Sometimes components are far apart, or many parts of the app rely on the same data. That’s when a store is best.
Example – Pinia Store
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
},
},
})
<!-- Any component -->
<script setup lang="ts">
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment">Count is {{ counter.count }}</button>
</template>
Use a store when:
-
Data needs to be shared across multiple parts of the app
-
State is central and reactive
-
You want devtools integration, persistence, or modules
My Rules of Thumb
I use:
-
emitfor parent–child interaction -
provide/inject for context-style values
-
Pinia when state is used in many places or gets too complex
Final Thoughts
Don’t overengineer communication. Start with props and emit. When things get messy, reach for provide/inject or a store.
Clear data flow = better code and easier debugging.
Bartłomiej Nowak
Programmer
Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.