Provide Inject
Vue 3 provide/inject for dependency injection, deeply nested component communication, and plugin design
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 linesProvide/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 uniqueSymbolkeys withInjectionKey<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, orsetTimeout, where there is no active component instance. Inject must be called synchronously duringsetup(). -
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 componentsinject(key, defaultValue?)— retrieves a provided value from the nearest ancestorInjectionKey<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
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
- Always use
InjectionKeysymbols — string keys are error-prone and untyped. Symbol keys withInjectionKey<T>give full type safety. - Wrap inject in a composable — a
useX()wrapper provides better error messages and makes the dependency explicit in consumer code. - Provide
readonlystate — if consumers should not mutate provided state, wrap refs withreadonly()and expose mutation methods separately. - Use provider components — encapsulate provide logic and any setup UI (loading states, error boundaries) in a dedicated provider component.
- Provide reactive values — provide refs or reactive objects so descendants stay in sync. Providing a plain value is a snapshot that never updates.
- Document injection requirements — make it clear in component or composable docs which provider must be an ancestor.
Common Pitfalls
- Injecting without a provider —
inject()returnsundefinedif no ancestor provides the key. Always provide a default value or throw an explicit error. - Providing non-reactive values —
provide('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
provideshadows a farther one. This can be a feature (nested themes) or a bug if unintended. - Using inject outside of setup —
inject()must be called synchronously insidesetup()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
Related Skills
Composables
Building reusable custom composables in Vue 3 with proper typing, lifecycle management, and state sharing
Composition API
Patterns and best practices for Vue 3 Composition API including refs, reactive, computed, watchers, and lifecycle hooks
Nuxt Routing
Nuxt 3 file-based routing, dynamic routes, layouts, middleware, and navigation patterns
Nuxt Server
Nuxt 3 server routes, API endpoints, server middleware, and Nitro engine patterns for full-stack development
Pinia
Pinia state management for Vue 3 including store design, actions, getters, plugins, and SSR hydration
Vue Router
Vue Router 4 advanced patterns including navigation guards, lazy loading, scroll behavior, and typed routes