Provide / inject

Vue · Practical · 9 min

Themes, current user, form context and component libraries rely on provide/inject to avoid prop drilling.

Passing a prop through five components that don't use it (prop drilling) is tedious. With provide/inject, an ancestor provides a value under a key and any descendant, however deep, injects it.

// Ancestor
provide('theme', theme)          // theme is a ref
// Descendant
const theme = inject('theme', ref('light'))   // second arg = default

Key points:

  • Provide refs (or reactive objects), not .value, so descendants see updates.
  • Keep mutations with the provider: provide a read-only view with readonly(theme) plus a function like setTheme, so it is clear who changes the data.
  • The nearest provider wins, so a subtree can override a value.
  • inject without a default returns undefined (and warns) when no ancestor provides the key.
  • app.provide(key, value) provides to the whole application; plugins use this.

For large apps, use Symbol keys to avoid collisions, and with TypeScript type them as InjectionKey<T>: export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme'). Put keys in a shared module.

Provide/inject suits component-tree-scoped context: a form providing validation state to its fields, a tabs component to its panels, a design system's theme. For global application state (cart, session) a store like Pinia is usually clearer and has devtools support.

Example

// keys.js
export const ThemeKey = Symbol('theme')

// ThemeProvider.vue  <script setup>
import { ref, readonly, provide } from 'vue'
import { ThemeKey } from './keys.js'
const theme = ref('dark')
provide(ThemeKey, {
  theme: readonly(theme),
  toggle: () => { theme.value = theme.value === 'dark' ? 'light' : 'dark' },
})

// DeepButton.vue  <script setup>  (any depth below the provider)
import { inject } from 'vue'
import { ThemeKey } from './keys.js'
const { theme, toggle } = inject(ThemeKey)
// <button :class="theme" @click="toggle">Theme: {{ theme }}</button>

Remember

  • provide(key, value) in an ancestor, inject(key, default) in any descendant
  • Provide refs plus updater functions; wrap in readonly to keep mutations in one place
  • Use Symbol / InjectionKey keys in a shared module

Watch out for

  • Providing count.value instead of count, so descendants receive a frozen snapshot.

Check yourself

  1. A layout provides 'theme' and a nested section also provides 'theme'. What does a component inside the section inject?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vuejs.org/guide/components/provide-inject.html