Provide and Inject

Intermediate
11 min

Provide and Inject

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.

The Basic Mechanism

An ancestor calls provide(key, value). Any descendant, at any depth, calls inject(key) to receive it:

vue
<!-- 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 Reactive State

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:

vue
<!-- 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.

Typed Injection Keys

String keys collide easily in large apps and give no type information. Export a Symbol wrapped in InjectionKey from a shared module:

typescript
// 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.

App-Level Provide

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:

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

Provide/Inject vs Pinia

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

Tips

  • Always co-locate the provider and its injection key in the same module so they cannot drift apart.
  • Call inject synchronously in setup or inside a composable; hasInjectionContext() tells a composable whether that is currently possible.
  • Prefer providing a small object of state plus functions over many separate keys.
Quick Quiz
Question 1 of 2

What happens when `inject('settings')` is called and no ancestor provided that key?

Key Takeaways

  • provide(key, value) in an ancestor and inject(key, default) in any descendant replace prop drilling.
  • Provided refs stay reactive; expose readonly state plus mutation functions.
  • Use Symbol keys typed with InjectionKey<T> for safety in TypeScript projects.
  • app.provide makes a value available to the whole application.
  • Provide/inject suits subtree and compound-component state; Pinia suits global state.

Next lesson: Dynamic Components, KeepAlive and Async Components — switch between components at runtime, preserve their state, and load them on demand.

Provide and Inject - Vue.js | CodeYourCraft | CodeYourCraft