Themes, current user, form context and component libraries rely on provide/inject to avoid prop drilling.
Passing a prop through five components that don't use it (prop drilling) is tedious. With provide/inject, an ancestor provides a value under a key and any descendant, however deep, injects it.
// Ancestor
provide('theme', theme) // theme is a ref
// Descendant
const theme = inject('theme', ref('light')) // second arg = defaultKey points:
- Provide refs (or reactive objects), not
.value, so descendants see updates. - Keep mutations with the provider: provide a read-only view with
readonly(theme)plus a function likesetTheme, so it is clear who changes the data. - The nearest provider wins, so a subtree can override a value.
injectwithout a default returnsundefined(and warns) when no ancestor provides the key.app.provide(key, value)provides to the whole application; plugins use this.
For large apps, use Symbol keys to avoid collisions, and with TypeScript type them as InjectionKey<T>: export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme'). Put keys in a shared module.
Provide/inject suits component-tree-scoped context: a form providing validation state to its fields, a tabs component to its panels, a design system's theme. For global application state (cart, session) a store like Pinia is usually clearer and has devtools support.
Example
// keys.js
export const ThemeKey = Symbol('theme')
// ThemeProvider.vue <script setup>
import { ref, readonly, provide } from 'vue'
import { ThemeKey } from './keys.js'
const theme = ref('dark')
provide(ThemeKey, {
theme: readonly(theme),
toggle: () => { theme.value = theme.value === 'dark' ? 'light' : 'dark' },
})
// DeepButton.vue <script setup> (any depth below the provider)
import { inject } from 'vue'
import { ThemeKey } from './keys.js'
const { theme, toggle } = inject(ThemeKey)
// <button :class="theme" @click="toggle">Theme: {{ theme }}</button>Remember
provide(key, value)in an ancestor,inject(key, default)in any descendant- Provide refs plus updater functions; wrap in
readonlyto keep mutations in one place - Use Symbol /
InjectionKeykeys in a shared module
Watch out for
- Providing
count.valueinstead ofcount, so descendants receive a frozen snapshot.