provide and inject pass a dependency through a component subtree without forwarding it through every intermediate prop. This fits stable contextual services such as form registration, theme controls, or a parent-owned coordination API. It should not become invisible global state for unrelated features.
provide and inject solve the prop drilling problem - passing data through many layers of components that don't need it. A parent component provides data, and any descendant (no matter how deep) can inject it directly.
Unlike props, provide/inject skips intermediate components entirely. It's Vue's built-in dependency injection system.
| Feature | Props | Provide/Inject | Pinia |
|---|---|---|---|
| Scope | Parent -> direct child | Ancestor -> any descendant | Global |
| Reactivity | Yes | Yes (with ref/reactive) | Yes |
| Best for | Direct parent-child | Plugin-like data, theme, locale | App-wide state |
<!-- App.vue - provides data to all descendants -->
<template>
<div :data-theme="theme">
<Navbar />
<Main />
<Footer />
</div>
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
// Provide reactive data
const theme = ref('light')
const locale = ref('en')
const user = ref({ name: 'Alice', role: 'admin' })
// Provide a function to update theme (keeps mutation in provider)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide(key, value)
provide('theme', readonly(theme)) // read-only to prevent mutation
provide('toggleTheme', toggleTheme) // function to update
provide('locale', locale)
provide('currentUser', readonly(user))
// Provide an object with multiple values
provide('appConfig', {
apiUrl: 'https://api.example.com',
version: '2.0.0',
features: { darkMode: true, notifications: true }
})
</script>
<!-- DeepChild.vue - can be nested 10 levels deep, still works -->
<template>
<div :class="`theme-${theme}`">
<p>Theme: {{ theme }}</p>
<p>User: {{ currentUser?.name }}</p>
<p>API: {{ appConfig?.apiUrl }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// inject(key, defaultValue)
const theme = inject('theme', 'light') // with default
const toggleTheme = inject('toggleTheme', () => {}) // with default
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
// inject returns undefined if not provided (no error)
const locale = inject('locale')
</script>
<!-- Symbol keys - avoid naming collisions in large apps -->
<!-- keys.js -->
<!-- export const THEME_KEY = Symbol('theme') -->
<!-- export const USER_KEY = Symbol('user') -->
<!-- Provider: provide(THEME_KEY, theme) -->
<!-- Consumer: const theme = inject(THEME_KEY) -->
// composables/useTheme.js - wrap provide/inject in composables
import { ref, provide, inject, readonly } from 'vue'
const THEME_KEY = Symbol('theme')
// Used in the provider component (App.vue or layout)
export function provideTheme() {
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
function setTheme(newTheme) {
theme.value = newTheme
}
provide(THEME_KEY, {
theme: readonly(theme),
toggleTheme,
setTheme,
})
return { theme, toggleTheme, setTheme }
}
// Used in any descendant component
export function useTheme() {
const context = inject(THEME_KEY)
if (!context) {
throw new Error('useTheme() must be used within a component that calls provideTheme()')
}
return context
}
// Usage in App.vue:
// import { provideTheme } from '@/composables/useTheme'
// provideTheme()
// Usage in any child:
// import { useTheme } from '@/composables/useTheme'
// const { theme, toggleTheme } = useTheme()
Use a Symbol as the injection key in larger applications and export the key with the dependency type. Provide a readonly view of state when descendants should request changes through explicit actions. Supply a meaningful default only when absence is a valid mode; otherwise fail early with a clear developer error.
The closest ancestor providing the same key wins, which enables scoped overrides in nested subtrees and tests. Keep mutations near the provider so ownership remains clear. Use Pinia instead when distant routes share a domain store and need devtools, persistence policy, or application-level lifecycle.
Explore 500+ free tutorials across 20+ languages and frameworks.