Vue.js: provide / inject
Last updated: 2026-10-20
provide and inject are Vue 3's cross-level communication APIs: an ancestor component provides data, and any descendant component—no matter how deep—can inject and use it. They solve the pain of "prop drilling."
Vue 3's provide supports reactive data (ref / reactive), and descendants react automatically once they inject it. This lesson gets you comfortable with the mechanism and 5 real-world scenarios.
1. What You'll Learn
- The basic
provide/injectsyntax - Using a Symbol as an inject key
- Reactive provide (ref / reactive)
- 2 ways to modify a provided value
- How it compares with props / event bus / Pinia
- 5 real-world scenarios (theme / user / i18n)
- 3 anti-patterns related to provide / inject
2. The Prop-Drilling Nightmare: Passing One "Theme Color" Through 5 Layers
(1) The Pain: App → Layout → Header → UserMenu → Button, 5 Layers of Pass-Through
Alice's admin dashboard needs to support theme switching:
<!-- ❌ Broken version: 5 layers of prop drilling -->
<!-- App.vue -->
<Layout :theme="theme" :user="user" :locale="locale">
<router-view />
</Layout>
<!-- Layout.vue -->
<Header :theme="theme" :user="user" :locale="locale" />
<!-- Header.vue -->
<UserMenu :theme="theme" :user="user" :locale="locale" />
<!-- UserMenu.vue -->
<ThemeButton :theme="theme" />
<LocaleSelector :locale="locale" />
Product manager Charlie adds 3 more config options:
"Alice, we also need a sidebar collapse setting, notification settings, and feature flags. That's 8 props through 5 layers of components—this is simply unmaintainable."
(2) The Vue provide / inject Fix: Ancestors Provide, Descendants Receive
<!-- App.vue ancestor -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
const user = ref({ name: 'Alice' })
const locale = ref('zh-CN')
// ✅ Provide once, every descendant can inject
provide('theme', theme)
provide('user', user)
provide('locale', locale)
</script>
<!-- UserMenu.vue any deep descendant -->
<script setup>
const { inject } = Vue
// ✅ Get it directly, no prop pass-through needed
const theme = inject('theme')
const user = inject('user')
const locale = inject('locale')
</script>
<template>
<button :class="theme">Toggle {{ user.name }}</button>
<p>{{ locale }}</p>
</template>
5 layers of nesting → 1 line of provide + 1 line of inject.
(3) The Payoff
After adopting provide / inject:
- Nesting depth: unlimited (any descendant can inject)
- Number of props: 5+ → 0 (no more pass-through)
- Adding a new setting: one
providecall - Maintainability: the ancestor manages global state in one place
3. Basic provide / inject Syntax
(1) provide: Supplying Data
<!-- App.vue ancestor component -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
const user = { name: 'Alice' }
// 1. Provide a static value
provide('appName', 'My Admin')
// 2. Provide reactive data (ref)
provide('theme', theme)
// 3. Provide reactive data (reactive)
provide('user', user)
// 4. Provide a method
provide('updateUser', (newUser) => {
user.value = newUser
})
</script>
(2) inject: Receiving Data
<!-- Child.vue any descendant component -->
<script setup>
const { inject } = Vue
// 1. Basic injection
const theme = inject('theme')
// 2. With a default value (used when no ancestor provides it)
const theme = inject('theme', 'light')
// 3. With a factory function as the default
const theme = inject('theme', () => 'light')
// 4. Type assertion (TypeScript)
const theme = inject<string>('theme', 'light')
</script>
(3) Complete Example
<!-- Ancestor.vue -->
<script setup>
const { ref, provide } = Vue
const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })
provide('count', count) // ref
provide('user', user) // reactive
provide('config', { theme: 'dark' }) // object
provide('reset', () => { count.value = 0 }) // method
</script>
<!-- DeepChild.vue (any depth) -->
<script setup>
const { inject } = Vue
const count = inject('count') // reactive
const user = inject('user') // reactive
const config = inject('config') // plain object
const reset = inject('reset') // function
// Modify
function increment() {
count.value++ // ✅ The ancestor reacts too
}
</script>
▶ Example: Ancestor provides a counter, deep child injects it (with a default)
▶ Try it YourselfOutput:
A single green-bordered panel inside the grey middle layer, first line "Default label: 0" — the ancestor never provides label, so the inject fallback kicks in.
Click "+1" twice → the number becomes 2; click "Reset" → back to 0. The middle Toolbar layer never passes any data.
4. Symbols as inject Keys
(1) Why Use a Symbol?
String keys collide easily (two provides with the same key overwrite each other). A Symbol is unique, so collisions are impossible.
<!-- keys.js - central place for all provide keys -->
<script>
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CONFIG_KEY = Symbol('config')
</script>
<!-- Ancestor.vue ancestor component -->
<script setup>
const { provide, ref } = Vue
import { THEME_KEY, USER_KEY, LOCALE_KEY } from './keys'
provide(THEME_KEY, ref('light'))
provide(USER_KEY, ref({ name: 'Alice' }))
provide(LOCALE_KEY, ref('zh-CN'))
</script>
<!-- DeepChild.vue any descendant -->
<script setup>
const { inject } = Vue
import { THEME_KEY } from './keys'
// ✅ Type-safe: you must use the Symbol to inject
const theme = inject(THEME_KEY)
const user = inject(USER_KEY)
</script>
(2) 5 Advantages
| Advantage | Description |
|---|---|
| Uniqueness | Symbols are globally unique, so they never collide |
| Type safety | TypeScript can infer the type precisely |
| Centralized management | All keys live in a single file |
| Refactor friendly | Renaming a key doesn't affect anything else |
| Readability | The key name tells you what it's for |
5. Reactive provide in Detail
(1) 3 provide Patterns
<!-- Ancestor.vue -->
<script setup>
const { ref, reactive, provide } = Vue
// 1. Provide a ref (reactive)
const count = ref(0)
provide('count', count)
// 2. Provide a reactive object (reactive)
const user = reactive({ name: 'Alice' })
provide('user', user)
// 3. Provide a read-only object (not reactive)
const config = { theme: 'dark' }
provide('config', config)
</script>
(2) 2 Ways to Modify the Value
<!-- Way 1: the ancestor modifies it directly (recommended) -->
<!-- Ancestor.vue -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
provide('theme', theme)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<!-- Way 2: a descendant modifies it through the injected reference -->
<!-- DeepChild.vue -->
<script setup>
const { inject } = Vue
const theme = inject('theme')
function toggleTheme() {
theme.value = 'dark' // ✅ You're changing the ref, so the ancestor and other descendants react
}
</script>
(3) 5 Reactive Scenarios
| Scenario | Approach |
|---|---|
| Theme switching | ref + provide |
| User login | reactive + provide |
| i18n locale | ref + provide + a switch method |
| Global loading | ref(false) + provide |
| Global notifications | reactive + provide |
6. 5 Real-World Scenarios
(1) Scenario 1: Theme Switching
// keys.js
export const THEME_KEY = Symbol('theme')
<!-- App.vue ancestor -->
<script setup>
const { ref, provide } = Vue
import { THEME_KEY } from './keys'
const theme = ref('light')
provide(THEME_KEY, theme)
</script>
<!-- ThemeButton.vue descendant -->
<script setup>
const { inject } = Vue
import { THEME_KEY } from './keys'
const theme = inject(THEME_KEY)
</script>
<template>
<button @click="theme.value = theme === 'light' ? 'dark' : 'light'">
{{ theme === 'light' ? '🌞' : '🌙' }}
</button>
</template>
(2) Scenario 2: User Authentication
// keys.js
export const AUTH_KEY = Symbol('auth')
<!-- App.vue -->
<script setup>
const { reactive, provide } = Vue
import { AUTH_KEY } from './keys'
const auth = reactive({
user: null,
isLoggedIn: false,
login(credentials) { /* API call */ },
logout() { this.user = null; this.isLoggedIn = false }
})
provide(AUTH_KEY, auth)
</script>
<!-- UserMenu.vue descendant -->
<script setup>
const { inject } = Vue
import { AUTH_KEY } from './keys'
const auth = inject(AUTH_KEY)
</script>
<template>
<div v-if="auth.isLoggedIn">{{ auth.user.name }}</div>
<button v-else @click="auth.login(creds)">Login</button>
</template>
(3) Scenario 3: i18n
// keys.js
export const I18N_KEY = Symbol('i18n')
<!-- App.vue -->
<script setup>
const { ref, provide, computed } = Vue
import { I18N_KEY } from './keys'
const messages = {
'en-US': { hello: 'Hello', welcome: 'Welcome' },
'es-ES': { hello: 'Hola', welcome: 'Bienvenido' }
}
const locale = ref('en-US')
const t = computed(() => (key) => messages[locale.value][key])
provide(I18N_KEY, { locale, t })
</script>
<!-- Hello.vue descendant -->
<script setup>
const { inject } = Vue
import { I18N_KEY } from './keys'
const { t } = inject(I18N_KEY)
</script>
<template>
<h1>{{ t('hello') }}, {{ t('welcome') }}</h1>
</template>
(4) Scenario 4: Global Loading
<!-- App.vue -->
<script setup>
const { ref, provide } = Vue
const loading = ref(false)
provide('loading', loading)
</script>
<!-- AnyComponent.vue descendant -->
<script setup>
const { inject } = Vue
const loading = inject('loading')
async function fetchData() {
loading.value = true
await fetch('/api/data')
loading.value = false
}
</script>
<template>
<button @click="fetchData">Refresh</button>
</template>
(5) Scenario 5: Theme + Business Data
<!-- App.vue -->
<script setup>
const { ref, reactive, provide } = Vue
const appState = reactive({
theme: 'light',
user: { name: 'Alice' },
permissions: ['read', 'write'],
config: { sidebar: true }
})
provide('appState', appState)
</script>
▶ Example: Centralized keys + a single provide
▶ Expand runnable example (89 lines)
Output:
Two dashed-border panels are rendered (DeepChild, then GrandChild).
Inside the innermost panel you see User: Alice (admin), i18n: Home | Cart, Cart: 1 item(s), $999, and a green button reading Toggle theme (current: light).
Clicking the button flips the whole page to the dark palette and the label becomes dark.
DeepChild middle layer (does not consume provide)
GrandChild middle layer
User: Alice (admin)
i18n: Home | Cart
Cart: 1 item(s), $999
[ Toggle theme (current: light) ]
# after clicking the button: background #1f2937, text #f9fafb
User: Alice (admin)
i18n: Home | Cart
Cart: 1 item(s), $999
[ Toggle theme (current: dark) ]
7. provide / inject vs Other Approaches
(1) Comparing 5 Communication Patterns
| Dimension | props/emit | provide/inject | event bus | Pinia | mitt |
|---|---|---|---|---|---|
| Parent-child | ⭐⭐⭐⭐⭐ | - | - | - | - |
| Cross-level | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Sibling | ❌ (needs a parent relay) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Reactive | ✅ | ✅ | ❌ (manual) | ✅ | ❌ |
| Type safety | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| DevTools | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Learning curve | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
(2) 5 Situations and What to Pick
| Situation | Recommended |
|---|---|
| Parent-child communication | props/emit |
| Cross-level global state (theme/auth/i18n) | provide/inject |
| Global state in mid-to-large apps | Pinia (covered in Phase 4.2) |
| A simple event bus | mitt / tiny-emitter |
| Complex business state management | Pinia + Vue Router |
8. Putting It Together: Symbol inject Keys and a Pitfall Cheat Sheet
▶ Example: Symbol inject keys (avoiding collisions)
▶ Try it YourselfOutput:
Three lines are rendered: THEME_KEY: light, ANOTHER_THEME_KEY: dark, and a grey hint that reads Both Symbols share the same description but never collide.
The two values stay independent, proving the keys don't collide.
THEME_KEY: light
ANOTHER_THEME_KEY: dark
Both Symbols share the same description but never collide
(1) 📊 5 Common Mistakes at a Glance
| Mistake | Symptom | Fix |
|---|---|---|
| String key collision | Overwritten | Use a Symbol |
| Providing from a non-ancestor | undefined | Check the component hierarchy |
| Mutating a readonly provide | Warning | Ancestor mutates, descendants read |
| Forgetting the default value | undefined | inject(key, defaultValue) |
| Too many provides | Hard to maintain | Centralize them in keys.js |
(2) 📊 5 Performance Comparisons
| Pattern | Performance | Best for |
|---|---|---|
provide('key', value) |
⭐⭐⭐⭐⭐ | Static values |
provide('key', ref) |
⭐⭐⭐⭐⭐ | Reactive values |
provide('key', reactive) |
⭐⭐⭐⭐ | Complex objects |
provide('key', computed) |
⭐⭐⭐⭐⭐ | Derived values |
provide('key', function) |
⭐⭐⭐⭐ | Action methods |
❓ FAQ
provide('key', ref) is reactive; provide('key', {a: 1}) is not (plain object). Providing a ref / reactive is recommended.provide must be called at the top level of <script setup> (not inside lifecycle hooks), otherwise it won't take effect.ref.value, but it's better to modify it only in the ancestor (through a provided method). Mutating the ref directly makes the data flow unclear.src/keys.js, exporting all Symbols. Every component that needs them imports from that file.📖 Summary
- provide / inject is Vue 3's cross-level communication API
- provide supplies (ancestor), inject receives (any descendant)
- 5 kinds of provided values: static value / ref / reactive / computed / function
- Symbol keys avoid collisions and are centralized in keys.js
- 5 real-world scenarios: theme / auth / i18n / loading / business data
- Compared with props: props for parent-child, provide for cross-level
- Compared with Pinia: provide for small projects, Pinia for large ones
📝 Exercises
-
Basic (difficulty ⭐) Build a simple theme switcher:
- Define THEME_KEY in keys.js
- App.vue provides theme (ref) + a setTheme method
- ThemeButton.vue injects theme + setTheme
- Clicking the button toggles light/dark
-
Intermediate (difficulty ⭐⭐) Implement user authentication with provide/inject:
- AUTH_KEY provides user + login + logout methods
- App.vue initializes user = null
- LoginPage.vue calls login(creds) to simulate signing in
- Header.vue renders different UI based on isLoggedIn
-
Challenge (difficulty ⭐⭐⭐) Implement a complete global state system for an e-commerce admin:
- 4 keys: THEME / USER / CART / LOCALE
- App.vue provides all 4 global states at once
- 5 components at different levels consume them (Header / Sidebar / ProductList / Cart / Footer)
- Cross-component communication: Header triggers logout → Sidebar updates automatically
- Theme switching + i18n switching + increasing/decreasing the cart
- Use strong TypeScript types
- All 5 provide patterns (value/ref/reactive/computed/function)