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


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:

VUE
<!-- ❌ 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

VUE
<!-- 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:


3. Basic provide / inject Syntax

(1) provide: Supplying Data

VUE
<!-- 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

VUE
<!-- 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

VUE
<!-- 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 Yourself

Output:

TEXT
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.

VUE
<!-- 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>
VUE
<!-- 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>
VUE
<!-- 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

VUE
<!-- 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

VUE
<!-- 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

JS
// keys.js
export const THEME_KEY = Symbol('theme')
VUE
<!-- 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

JS
// keys.js
export const AUTH_KEY = Symbol('auth')
VUE
<!-- 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

JS
// keys.js
export const I18N_KEY = Symbol('i18n')
VUE
<!-- 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

VUE
<!-- 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

VUE
<!-- 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)
▶ Try it Yourself

Output:

TEXT
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.
TEXT
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 Yourself

Output:

TEXT
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.
TEXT
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

Q provide or props—which should I use?
A Use props for direct parent-child communication (explicit, type-safe). Use provide for cross-level communication (3+ layers) to avoid prop drilling. Use Pinia for global state in large apps (Phase 4.2).
Q Is the provided data reactive?
A It depends on what you provide. provide('key', ref) is reactive; provide('key', {a: 1}) is not (plain object). Providing a ref / reactive is recommended.
Q Does provide have to be at the top level of setup?
A Yes. provide must be called at the top level of <script setup> (not inside lifecycle hooks), otherwise it won't take effect.
Q Can I modify a provided value after injecting it?
A You can change 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.
Q Where should Symbol keys be defined?
A In a dedicated file, src/keys.js, exporting all Symbols. Every component that needs them imports from that file.
Q provide / inject or Pinia?
A For small projects (fewer than 5 pieces of global state) provide/inject is enough. For mid-to-large projects (10+ pieces of global state) use Pinia—its DevTools support is better.
Q What happens to provided data when the ancestor unmounts?
A All descendants unmount with it (Vue component tree lifecycle). inject will look for a new ancestor above.

📖 Summary


📝 Exercises

  1. 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
  2. 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
  3. Challenge (difficulty ⭐⭐⭐) Implement a complete global state system for an e-commerce admin:

    1. 4 keys: THEME / USER / CART / LOCALE
    2. App.vue provides all 4 global states at once
    3. 5 components at different levels consume them (Header / Sidebar / ProductList / Cart / Footer)
    4. Cross-component communication: Header triggers logout → Sidebar updates automatically
    5. Theme switching + i18n switching + increasing/decreasing the cart
    6. Use strong TypeScript types
    7. All 5 provide patterns (value/ref/reactive/computed/function)
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏