provide and inject
Pass a value from an ancestor to any descendant without threading props through the components between them.
provide(key, value) / inject(key, defaultValue?, treatDefaultAsFactory?)provide and inject move a value from an ancestor component to any descendant without passing it through the components in between. The ancestor calls provide(key, value) during setup; any component below it calls inject(key) and receives the same value. The components in the middle declare nothing and do not know it happened.
Keeping it reactive
What travels is the value you hand over, so hand over a ref or a computed. A plain number is copied once and never changes. The convention is to provide a read-only view plus the functions that change it — { theme: computed(() => theme.value), setTheme } — so every mutation stays in the component that owns the state. readonly() around a ref does the same job for a single value.
Injection keys
A string key works and collides. Export a Symbol from a shared file instead, cast as InjectionKey<T>, and inject returns the right type with no annotation at the call site. inject(key, fallback) supplies a default; pass true as a third argument when the default is a factory that should be called.
Why this is not a store
The value reaches descendants only. A sibling branch of the tree cannot see it, and nothing outside a component can read or write it. That makes it right for a widget's internal context — a form telling its fields which group they are in. Application state any screen may need wants a store instead.
Example
<script setup lang="ts">
import { computed, provide, ref } from 'vue'
import { themeKey, type Theme } from '../keys/theme'
const theme = ref<Theme>('light')
function setTheme(next: Theme) {
theme.value = next
}
provide(themeKey, {
theme: computed(() => theme.value),
setTheme
})
</script>
<template>
<div :data-theme="theme">
<slot />
</div>
</template>
<style scoped>
div[data-theme='dark'] {
background: #111111;
color: #eeeeee;
}
</style>When to use it
- Let deeply nested field components read the form state their wrapper owns, without prop drilling.
- Give a widget's subcomponents shared context, such as the open accordion panel or the active tab.
- Hand every component a fixed application value, such as a content base path, from main.ts.
More examples
The typed key
A Symbol cannot collide with another library's key, and the `InjectionKey<T>` cast is what carries the type from the provider to every injector. The string passed to `Symbol()` is only a label that shows up in debugging output.
// src/keys/theme.ts
import type { ComputedRef, InjectionKey } from 'vue'
export type Theme = 'light' | 'dark'
export interface ThemeContext {
theme: ComputedRef<Theme>
setTheme: (next: Theme) => void
}
export const themeKey = Symbol('theme') as InjectionKey<ThemeContext>The consumer, several levels down
`inject` with a typed key returns `ThemeContext | undefined`, so the guard both gives a readable error and narrows the type for the rest of the file. In the template `theme` is unwrapped automatically; in script it would be `theme.value`.
<script setup lang="ts">
import { inject } from 'vue'
import { themeKey } from '../keys/theme'
const context = inject(themeKey)
if (!context) {
throw new Error('ThemeToggle must be used inside ThemeProvider')
}
const { theme, setTheme } = context
</script>
<template>
<button type="button" @click="setTheme(theme === 'dark' ? 'light' : 'dark')">
Switch to {{ theme === 'dark' ? 'light' : 'dark' }}
</button>
</template>
<style scoped>
button {
cursor: pointer;
}
</style>Providing to the whole application
`app.provide` makes a value injectable in every component of that application, with no provider component needed. Use it for things that are fixed for the whole app, such as a base path. It is still inject-only: nothing outside a component can read it.
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.provide('contentBasePath', '/data')
app.mount('#app')
Discussion