Props flow one level at a time. When a value is needed five components deep, passing it through every intermediate component ("prop drilling") clutters components that do not care about it. provide and inject let an ancestor make a value available to all of its descendants directly. After this lesson you will provide reactive state safely, use typed injection keys, provide at the application level, and know when a store is the better tool.
An ancestor calls provide(key, value). Any descendant, at any depth, calls inject(key) to receive it:
<!-- Layout.vue -->
<script setup>
import { provide } from 'vue'
provide('appName', 'Inventory Manager')
</script>
<!-- PageFooter.vue, rendered somewhere inside Layout -->
<script setup>
import { inject } from 'vue'
const appName = inject('appName')
const currency = inject('currency', 'USD') // default when nothing was provided
</script>Injection resolves by walking up the parent chain, so the nearest provider of a key wins. A component can provide and inject in the same setup. If no ancestor provides the key and no default is given, Vue logs a warning and inject returns undefined.
To compute a default lazily (for example an object), pass a factory and true as the third argument: inject('config', () => ({ debug: false }), true).
Providing a ref keeps the connection live: when the provider changes it, every injector re-renders. The recommended pattern is to keep mutations in the provider and expose functions for descendants to call, wrapping the state in readonly so nobody mutates it from below:
<!-- ThemeProvider.vue -->
<script setup>
import { ref, readonly, provide } from 'vue'
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', { theme: readonly(theme), toggleTheme })
</script>A consumer then does const { theme, toggleTheme } = inject('theme') and binds theme in its template. Because theme is a ref, it unwraps in the template but needs .value in script.
String keys collide easily in large apps and give no type information. Export a Symbol wrapped in InjectionKey from a shared module:
// src/keys.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
theme: Readonly<Ref<'light' | 'dark'>>
toggleTheme: () => void
}
export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')Now provide(themeKey, { theme, toggleTheme }) is type-checked, and inject(themeKey) returns ThemeContext | undefined. Since the return type includes undefined, either supply a default or check it; a small wrapper composable such as useTheme() that throws a clear error when the context is missing is a common pattern.
Values needed everywhere, such as an HTTP client or a feature-flag object, can be provided on the application instance in main.js instead of inside a component:
import { createApp } from 'vue'
import App from './App.vue'
import { createApiClient } from './api'
const app = createApp(App)
app.provide('api', createApiClient({ baseUrl: import.meta.env.VITE_API_URL }))
app.mount('#app')Every component in the app, including those rendered by Vue Router, can inject('api'). Plugins use the same mechanism internally.
| Use provide/inject when | Use Pinia when | | --- | --- | | The state belongs to a subtree (a form, a tab group, a wizard) | The state is global to the app | | A component library needs parent-child coordination (Tabs and Tab) | Multiple unrelated pages read and write the same data | | You want zero extra dependencies | You want devtools, persistence plugins and SSR hydration |
Provide/inject is the right tool for compound components: a <Tabs> component provides the active tab and a registration function, and each <Tab> injects them, so consumers can nest them freely.
inject synchronously in setup or inside a composable; hasInjectionContext() tells a composable whether that is currently possible.What happens when `inject('settings')` is called and no ancestor provided that key?
provide(key, value) in an ancestor and inject(key, default) in any descendant replace prop drilling.readonly state plus mutation functions.Symbol keys typed with InjectionKey<T> for safety in TypeScript projects.app.provide makes a value available to the whole application.Next lesson: Dynamic Components, KeepAlive and Async Components — switch between components at runtime, preserve their state, and load them on demand.