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

vYou’re sharing context (like theme, user, or layout data)

  • You don’t need reactivity across the whole app

Downsides:

  • Not reactive by default

vHarder 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:

  • emit for 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

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.

Recent Posts