On this page
provide() and inject(): dependency injection in Vue 3
The prop drilling problem
Props are the primary mechanism for passing data between components. However, when data needs to reach a deeply nested component, you must pass it through every intermediate component in the chain — even if those intermediary components do not use it themselves. This is called prop drilling.
App (has theme: 'dark')
└── Layout (passes theme as prop — doesn't use it)
└── Sidebar (passes theme as prop — doesn't use it)
└── NavLink (NEEDS theme — finally uses it!)Every time the data structure changes, you must update all the intermediate components. This tight coupling makes components harder to reuse and refactor.
provide() and inject()
Vue's solution is the provide / inject pair. An ancestor component provides a value, and any descendant — no matter how deep — can inject it directly, skipping all intermediate components.
// Ancestor
import { provide } from 'vue'
provide('key', value)
// Any descendant
import { inject } from 'vue'
const value = inject('key')There is no limit on depth. The descendant does not need to know which ancestor provided the value.
InjectionKey — type-safe injection
String keys work but lose type information. The recommended pattern is using InjectionKey<T> from Vue, which is a typed Symbol:
// injection-keys.ts — share this file between provider and consumer
import type { InjectionKey, Ref } from 'vue'
interface UserContext {
user: Ref<User | null>
logout: () => void
}
export const userKey: InjectionKey<UserContext> = Symbol('user')When you provide(userKey, {...}), TypeScript enforces the shape. When you inject(userKey), TypeScript infers UserContext | undefined automatically.
Providing reactive values
You can provide any reactive value — ref, reactive, computed, or a plain object containing them:
// Provider
const count = ref(0)
const doubled = computed(() => count.value * 2)
provide('counter', {
count: readonly(count), // expose as readonly to prevent direct mutation
doubled,
increment: () => count.value++,
})
// Consumer
const { count, doubled, increment } = inject('counter')!Best practice: provide the state as readonly() and expose mutation functions. This keeps mutations centralized in the provider.
Default values for inject()
inject() returns T | undefined unless you provide a default value. Provide a fallback for optional contexts:
// Option 1: default value (non-reactive)
const theme = inject('theme', 'light')
// Option 2: factory function for expensive defaults
const config = inject('config', () => ({ debug: false }), true)
// The third argument `true` means "treat second arg as a factory"
// Option 3: throw if not found
const required = inject('mustExist')
if (!required) throw new Error('This component must be inside <Provider>')App-level provide
You can provide values at the application level in main.ts, making them available to every component:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// App-level provides — available everywhere
app.provide('apiBaseUrl', 'https://api.example.com')
app.provide('version', '2.0.0')
app.mount('#app')Provide/inject in composables
The pattern becomes especially powerful when wrapped in composables:
// useTheme.ts
import { provide, inject, ref, readonly } from 'vue'
import type { InjectionKey } from 'vue'
type Theme = 'light' | 'dark'
interface ThemeContext {
theme: Readonly<Ref<Theme>>
setTheme: (t: Theme) => void
}
const themeKey: InjectionKey<ThemeContext> = Symbol('theme')
// Call in the providing ancestor
export function provideTheme() {
const theme = ref<Theme>('light')
function setTheme(t: Theme) { theme.value = t }
provide(themeKey, { theme: readonly(theme), setTheme })
}
// Call in any descendant
export function useTheme() {
const ctx = inject(themeKey)
if (!ctx) throw new Error('useTheme must be used inside a ThemeProvider')
return ctx
}Usage:
// In ThemeProvider.vue setup:
provideTheme()
// In any descendant:
const { theme, setTheme } = useTheme()This composable pattern eliminates string-key typos and centralizes the injection logic.
When to use provide/inject vs other patterns
| Pattern | Best for |
|---|---|
| Props | Direct parent-child communication, explicit data flow |
| Emits | Child-to-parent events, explicit feedback |
| provide/inject | Subtree-scoped shared context (forms, themes, localization) |
| Pinia | Global app state, DevTools, persistence |
| Composables | Reusable logic without shared state |
A form context is the canonical example for provide/inject — the <Form> component provides validation state and submission handlers, and deeply nested <Field> components inject them without prop drilling:
// Form.vue
const formContext = reactive({
values: {},
errors: {},
isSubmitting: false,
register: (name: string) => { /* ... */ },
})
provide(formContextKey, formContext)
// Field.vue (deeply nested)
const form = inject(formContextKey)!
// Access form.values, form.errors, form.registerPractice
- Build a ThemeProvider: Create a
ThemeProvider.vuethat provides athemeref and atoggleThemefunction viaInjectionKey. Create aThemeToggle.vuedeeply nested inside it that callsinject()to toggle the theme. - Form context: Create a simple form with a
Form.vueparent and threeField.vuechildren. Use provide/inject so eachFieldcan register itself, access the current value, and display validation errors without any props. - App-level provide: In
main.ts, provide the app version string. Display it in aFooter.vuecomponent usinginject().
In the next lesson, we will explore Vue Router — the official routing library for building multi-page Vue applications with navigation guards and lazy loading.
<script setup lang="ts">
import { ref, provide, readonly } from 'vue'
// Define an InjectionKey for type safety
import { themeKey } from './injection-keys'
const theme = ref<'light' | 'dark'>('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide() makes values available to ALL descendants
// readonly() prevents child components from mutating the ref directly
provide(themeKey, {
theme: readonly(theme),
toggleTheme,
})
</script>
<template>
<div :data-theme="theme">
<slot />
</div>
</template>
Sign in to track your progress