Skip to main content
Technology & EngineeringVue303 lines

Provide Inject

Vue 3 provide/inject for dependency injection, deeply nested component communication, and plugin design

Quick Summary25 lines
You are an expert in Vue 3's provide/inject system for dependency injection, component communication across deep hierarchies, and building plugin APIs.

## Key Points

- **`provide(key, value)`** — makes a value available to all descendant components
- **`inject(key, defaultValue?)`** — retrieves a provided value from the nearest ancestor
- **`InjectionKey<T>`** — a typed symbol used as a key for type-safe provide/inject
- **Reactivity** — provided refs and reactive objects remain reactive in descendants
- **App-level provide** — `app.provide()` makes values available to every component in the app
1. **Always use `InjectionKey` symbols** — string keys are error-prone and untyped. Symbol keys with `InjectionKey<T>` give full type safety.
2. **Wrap inject in a composable** — a `useX()` wrapper provides better error messages and makes the dependency explicit in consumer code.
3. **Provide `readonly` state** — if consumers should not mutate provided state, wrap refs with `readonly()` and expose mutation methods separately.
4. **Use provider components** — encapsulate provide logic and any setup UI (loading states, error boundaries) in a dedicated provider component.
5. **Provide reactive values** — provide refs or reactive objects so descendants stay in sync. Providing a plain value is a snapshot that never updates.
6. **Document injection requirements** — make it clear in component or composable docs which provider must be an ancestor.
- **Injecting without a provider** — `inject()` returns `undefined` if no ancestor provides the key. Always provide a default value or throw an explicit error.

## Quick Example

```ts
// main.ts
app.use(analyticsPlugin, { apiKey: 'abc123' })
```
skilldb get vue-skills/Provide InjectFull skill: 303 lines
Paste into your CLAUDE.md or agent config

Provide/Inject and Dependency Injection — Vue.js

You are an expert in Vue 3's provide/inject system for dependency injection, component communication across deep hierarchies, and building plugin APIs.

Core Philosophy

Provide/inject solves the prop drilling problem by letting ancestor components make values available to any descendant, regardless of depth. But its power carries a design responsibility: every provided value is an implicit dependency. Unlike props, which create a visible contract at each component boundary, injected values are invisible in the component's interface. This makes provide/inject the right tool for cross-cutting concerns (themes, i18n, auth) and the wrong tool for passing data one or two levels down.

The key design principle is to treat provide/inject as a dependency injection system, not a data passing mechanism. The best patterns wrap injected values in composables (useNotification(), useTheme()) that throw clear errors when the required provider is missing. This moves the implicit dependency into an explicit function call with runtime validation, giving consumers discoverability and providers confidence that their API is used correctly.

Reactivity flows naturally through provide/inject — provided refs and reactive objects remain reactive in descendants. But this only works when you provide the ref itself, not its current value. Combined with readonly() for one-way data flow and explicit mutation methods for controlled updates, provide/inject becomes a clean mechanism for building provider components that manage shared state for an entire subtree.

Anti-Patterns

  • String Key Collisions — using plain string keys like 'config' or 'theme' that silently collide when multiple libraries or components provide values under the same name. Always use unique Symbol keys with InjectionKey<T> for type safety.

  • Providing Snapshot Values Instead of Refs — writing provide('count', count.value) which provides the current number, not the reactive ref. Descendants receive a frozen value that never updates. Provide the ref directly.

  • Using Inject Outside Setup — calling inject() inside an async callback, event handler, or setTimeout, where there is no active component instance. Inject must be called synchronously during setup().

  • Deep Hierarchies of Implicit Dependencies — building components that require three or four nested providers to function, each injecting from the one above. This creates fragile, hard-to-test component trees. Flatten the dependency structure or merge related providers.

  • Providing Without Readonly Protection — exposing mutable refs to descendants without wrapping them in readonly(), allowing any descendant to modify shared state in unpredictable ways. Provide read-only state with explicit mutation methods.

Overview

provide and inject enable ancestor components to serve as dependency providers for all descendants, regardless of nesting depth. This avoids prop drilling and enables clean plugin and library APIs. Combined with TypeScript InjectionKey symbols, it delivers type-safe dependency injection.

Core Concepts

  • provide(key, value) — makes a value available to all descendant components
  • inject(key, defaultValue?) — retrieves a provided value from the nearest ancestor
  • InjectionKey<T> — a typed symbol used as a key for type-safe provide/inject
  • Reactivity — provided refs and reactive objects remain reactive in descendants
  • App-level provideapp.provide() makes values available to every component in the app

Implementation Patterns

Basic Provide/Inject

<!-- ParentComponent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const theme = ref<'light' | 'dark'>('light')

provide('theme', theme)

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

provide('toggleTheme', toggleTheme)
</script>
<!-- DeeplyNestedChild.vue -->
<script setup lang="ts">
import { inject, type Ref } from 'vue'

const theme = inject<Ref<'light' | 'dark'>>('theme')
const toggleTheme = inject<() => void>('toggleTheme')
</script>

<template>
  <div :class="theme">
    <button @click="toggleTheme?.()">Toggle Theme</button>
  </div>
</template>

Type-Safe Injection Keys

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

export interface NotificationService {
  show: (message: string, type?: 'info' | 'error' | 'success') => void
  dismiss: (id: string) => void
  notifications: Ref<Array<{ id: string; message: string; type: string }>>
}

export const NotificationKey: InjectionKey<NotificationService> =
  Symbol('NotificationService')

export interface AppConfig {
  apiBase: string
  appName: string
  features: Record<string, boolean>
}

export const AppConfigKey: InjectionKey<AppConfig> = Symbol('AppConfig')
<!-- Provider -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { NotificationKey, type NotificationService } from '@/injection-keys'

const notifications = ref<Array<{ id: string; message: string; type: string }>>([])

const service: NotificationService = {
  notifications,
  show(message, type = 'info') {
    const id = crypto.randomUUID()
    notifications.value.push({ id, message, type })
    setTimeout(() => service.dismiss(id), 5000)
  },
  dismiss(id) {
    notifications.value = notifications.value.filter(n => n.id !== id)
  },
}

provide(NotificationKey, service)
</script>
<!-- Consumer — fully typed without manual annotation -->
<script setup lang="ts">
import { inject } from 'vue'
import { NotificationKey } from '@/injection-keys'

const notifier = inject(NotificationKey)!
// notifier is typed as NotificationService

notifier.show('Item saved', 'success')
</script>

Composable Wrapper Pattern

// composables/useNotification.ts
import { inject } from 'vue'
import { NotificationKey, type NotificationService } from '@/injection-keys'

export function useNotification(): NotificationService {
  const service = inject(NotificationKey)
  if (!service) {
    throw new Error(
      'useNotification() requires a NotificationProvider ancestor. ' +
      'Wrap your app or route in <NotificationProvider>.'
    )
  }
  return service
}
<!-- Usage becomes clean and safe -->
<script setup lang="ts">
import { useNotification } from '@/composables/useNotification'

const { show } = useNotification()
show('Hello!', 'info')
</script>

App-Level Provide

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { AppConfigKey } from '@/injection-keys'

const app = createApp(App)

app.provide(AppConfigKey, {
  apiBase: import.meta.env.VITE_API_BASE,
  appName: 'My App',
  features: {
    darkMode: true,
    betaFeatures: false,
  },
})

app.mount('#app')

Read-Only Provided State

<script setup lang="ts">
import { provide, ref, readonly } from 'vue'

const count = ref(0)

// Descendants can read but not mutate — mutations go through provided methods
provide('count', readonly(count))
provide('increment', () => { count.value++ })
</script>

Provider Component Pattern

<!-- components/ThemeProvider.vue -->
<script setup lang="ts">
import { provide, ref, computed } from 'vue'

type Theme = 'light' | 'dark'

const props = withDefaults(defineProps<{
  initial?: Theme
}>(), {
  initial: 'light',
})

const theme = ref<Theme>(props.initial)

const isDark = computed(() => theme.value === 'dark')

function setTheme(t: Theme) {
  theme.value = t
}

function toggle() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

provide('theme', { theme, isDark, setTheme, toggle })
</script>

<template>
  <div :data-theme="theme">
    <slot />
  </div>
</template>
<!-- App.vue -->
<template>
  <ThemeProvider initial="dark">
    <RouterView />
  </ThemeProvider>
</template>

Plugin Using Provide

// plugins/analytics.ts
import type { App, InjectionKey } from 'vue'

export interface AnalyticsService {
  track: (event: string, properties?: Record<string, unknown>) => void
  page: (name: string) => void
}

export const AnalyticsKey: InjectionKey<AnalyticsService> = Symbol('Analytics')

export const analyticsPlugin = {
  install(app: App, options: { apiKey: string }) {
    const service: AnalyticsService = {
      track(event, properties) {
        console.log(`[Analytics] ${event}`, properties)
        // Send to analytics service
      },
      page(name) {
        console.log(`[Analytics] Page view: ${name}`)
      },
    }

    app.provide(AnalyticsKey, service)
  },
}
// main.ts
app.use(analyticsPlugin, { apiKey: 'abc123' })

Best Practices

  1. Always use InjectionKey symbols — string keys are error-prone and untyped. Symbol keys with InjectionKey<T> give full type safety.
  2. Wrap inject in a composable — a useX() wrapper provides better error messages and makes the dependency explicit in consumer code.
  3. Provide readonly state — if consumers should not mutate provided state, wrap refs with readonly() and expose mutation methods separately.
  4. Use provider components — encapsulate provide logic and any setup UI (loading states, error boundaries) in a dedicated provider component.
  5. Provide reactive values — provide refs or reactive objects so descendants stay in sync. Providing a plain value is a snapshot that never updates.
  6. Document injection requirements — make it clear in component or composable docs which provider must be an ancestor.

Common Pitfalls

  • Injecting without a providerinject() returns undefined if no ancestor provides the key. Always provide a default value or throw an explicit error.
  • Providing non-reactive valuesprovide('count', count.value) provides the current number, not the ref. Provide the ref itself: provide('count', count).
  • String key collisions — two libraries using provide('config', ...) collide silently. Use unique Symbols.
  • Overriding in intermediate components — a closer ancestor's provide shadows a farther one. This can be a feature (nested themes) or a bug if unintended.
  • Using inject outside of setupinject() must be called synchronously inside setup() or <script setup>. Calling it inside an async callback or event handler fails.
  • Circular dependencies — provider A injecting from provider B which injects from A creates impossible dependency chains. Restructure the hierarchy or merge the providers.

Install this skill directly: skilldb add vue-skills

Get CLI access →