iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…