Provide/Inject allows ancestor components to serve as dependency providers for all descendants, avoiding "prop drilling" through intermediate components.

The Problem: Prop Drilling

Grandparent (has theme)
    └─ Parent (just passes theme)
        └─ Child (just passes theme)
            └─ GrandChild (uses theme)

Every intermediate component must declare and pass the prop!

The Solution: Provide/Inject

Grandparent (provides theme)
    └─ Parent (unaware of theme)
        └─ Child (unaware of theme)
            └─ GrandChild (injects theme)

Basic Usage

Provider Component

vue
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const user = ref({ name: 'Alice', role: 'admin' })

// Provide values
provide('theme', theme)
provide('user', user)

// Provide methods too
function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
provide('toggleTheme', toggleTheme)
</script>

Consumer Component

vue
<script setup>
import { inject } from 'vue'

// Inject values
const theme = inject('theme')
const user = inject('user')
const toggleTheme = inject('toggleTheme')
</script>

<template>
  <div :class="theme">
    <p>Welcome, {{ user.name }}</p>
    <button @click="toggleTheme">Toggle Theme</button>
  </div>
</template>

Default Values

vue
<script setup>
import { inject } from 'vue'

// With default value
const theme = inject('theme', 'light')

// With factory function (for expensive defaults)
const heavyDefault = inject('heavy', () => createExpensiveObject(), true)
</script>

Providing at App Level

typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// App-wide provides
app.provide('appName', 'My App')
app.provide('apiBase', 'https://api.example.com')

app.mount('#app')

Reactivity with Provide/Inject

Provided refs stay reactive:

vue
<!-- Provider -->
<script setup>
import { ref, provide } from 'vue'

const count = ref(0)
provide('count', count)  // Reactive!

setInterval(() => count.value++, 1000)
</script>

<!-- Consumer -->
<script setup>
import { inject } from 'vue'

const count = inject('count')  // Updates automatically!
</script>

<template>
  <p>Count: {{ count }}</p>  <!-- Updates every second -->
</template>

Read-Only Provisions

Prevent consumers from mutating:

vue
<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)

// Provide read-only version
provide('count', readonly(count))

// Provide update method separately
provide('incrementCount', () => count.value++)
</script>

Working with TypeScript

Using Injection Keys

typescript
// keys.ts
import type { InjectionKey, Ref } from 'vue'

export type Theme = 'light' | 'dark'

export const themeKey: InjectionKey<Ref<Theme>> = Symbol('theme')
export const toggleThemeKey: InjectionKey<() => void> = Symbol('toggleTheme')
vue
<!-- Provider -->
<script setup lang="ts">
import { ref, provide } from 'vue'
import { themeKey, toggleThemeKey, type Theme } from './keys'

const theme = ref<Theme>('dark')

provide(themeKey, theme)
provide(toggleThemeKey, () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
</script>

<!-- Consumer -->
<script setup lang="ts">
import { inject } from 'vue'
import { themeKey, toggleThemeKey } from './keys'

const theme = inject(themeKey)!  // Ref<Theme>
const toggleTheme = inject(toggleThemeKey)!  // () => void
</script>

When to Use Provide/Inject

✅ Good use cases:

  • Theme/locale across app
  • Auth/user state
  • Shared configuration
  • Plugin data
  • Component library internals

❌ Not ideal for:

  • Simple parent-child communication (use props)
  • Global state management (use Pinia)
  • Sibling communication (use events or state)
✓ Completed