provide / inject
provide / inject is Vues built-in dependency injection: an ancestor exposes a value, and any descendant component reaches it without passing props through every level in between. It is the right tool for cross-cutting context (theme, current user, telemetry client, modal stack) — not as a general state-management replacement.
Provide a typed service and consume it deep in the tree
EXAMPLE
<!-- App.vue — provides a logger and a theme controller -->
<script setup lang='ts'>
import { provide, ref, type InjectionKey } from 'vue';
// 1) Strongly-typed injection keys — recommended over plain strings
export interface Logger {
info(msg: string, meta?: Record<string, unknown>): void;
warn(msg: string, meta?: Record<string, unknown>): void;
}
export const LoggerKey: InjectionKey<Logger> = Symbol('Logger');
export interface ThemeAPI {
current: Readonly<Ref<'light' | 'dark'>>;
toggle(): void;
}
export const ThemeKey: InjectionKey<ThemeAPI> = Symbol('Theme');
// 2) Provide a concrete logger implementation
const logger: Logger = {
info: (m, meta) => console.info('[ui]', m, meta ?? ''),
warn: (m, meta) => console.warn('[ui]', m, meta ?? ''),
};
provide(LoggerKey, logger);
// 3) Provide a reactive theme — descendants see updates automatically
const theme = ref<'light' | 'dark'>('light');
provide(ThemeKey, {
current: theme,
toggle: () => (theme.value = theme.value === 'light' ? 'dark' : 'light'),
});
</script>
<template>
<main :class='[theme === "dark" ? "bg-slate-900 text-white" : "bg-white"]'>
<Toolbar />
<Catalog />
</main>
</template>
<!-- Toolbar.vue — reads the theme service -->
<script setup lang='ts'>
import { inject } from 'vue';
import { ThemeKey } from './App.vue';
const theme = inject(ThemeKey);
if (!theme) throw new Error('ThemeKey must be provided by App.vue');
</script>
<template>
<button @click='theme.toggle()'>Theme: {{ theme.current.value }}</button>
</template>
<!-- ProductCard.vue — deep child uses the logger without prop drilling -->
<script setup lang='ts'>
import { inject } from 'vue';
import { LoggerKey } from './App.vue';
const log = inject(LoggerKey)!; // ! because we require it
function onAdded(productId: string) {
log.info('product.added', { productId });
}
</script>
<template><button @click='onAdded("sku-1")'>Add to cart</button></template>
<!-- ❌ Anti-pattern: provide(KEY, plainObject) — mutations on the
plain object are NOT reactive. Always provide a ref / reactive,
or expose getters and setters as part of the injected API. -->
Why it matters
For tree-wide state (router, store, theme), provide/inject is exactly right. For app-wide mutable state that any component might write to, reach for Pinia instead — it tracks subscriptions and supports devtools timelines, things ad-hoc inject does not.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Parent
provide('theme', 'dark');
// Descendant
const theme = inject('theme');
Try it Yourself »
Discussion
Loading…