provide / inject — Dependency Injection
TL;DR
provide makes a value available to all descendants of a component; inject reads it. The Vue equivalent of React’s Context — same use case (avoiding prop drilling), same caveats (overuse turns the tree into a global). Senior topics: reactivity of provided values, typed injection keys (InjectionKey<T>), default values, plugin/library patterns, and how Pinia subsumes a lot of what people previously did with provide/inject.
In depth
Basic example.
<!-- Ancestor.vue -->
<script setup>
import { provide, ref } from "vue";
const theme = ref("dark");
provide("theme", theme);
</script>
<!-- Descendant (any depth) -->
<script setup>
import { inject } from "vue";
const theme = inject("theme"); // Ref<string> | undefined
console.log(theme?.value);
</script>The key is a string; the value is anything (often a ref so descendants react to changes). No props pass through intermediate components.
Typed injection — InjectionKey<T>.
Use a Symbol typed as InjectionKey<T> so TS narrows the inject’s return type.
// keys.ts
import type { InjectionKey, Ref } from "vue";
export const ThemeKey: InjectionKey<Ref<"light" | "dark">> = Symbol("theme");// Ancestor
provide(ThemeKey, theme);
// Descendant
// Ref<"light" | "dark"> | undefined
const theme = inject(ThemeKey);Symbols also avoid string-key collisions. Use them for any library/shared-app provide.
Default values for inject.
const theme = inject(ThemeKey, ref("light")); // Default if not provided
const config = inject("config", () => createDefault(), true); // Factory (third arg = treat as factory)The factory form runs only when the inject misses, useful for expensive defaults.
Reactive provide — make a value updateable from descendants?
Provide a ref + an updater function:
// Ancestor
const theme = ref("dark");
const toggleTheme = () => { theme.value = theme.value === "dark" ? "light" : "dark"; };
provide(ThemeKey, { theme, toggleTheme });
// Descendant
const { theme, toggleTheme } = inject(ThemeKey)!;Or expose a readonly(theme) to enforce one-way reads + the updater being the only mutation path:
provide(ThemeKey, { theme: readonly(theme), toggleTheme });App-level provide.
app.provide(key, value) makes a value available everywhere without a Vue ancestor:
import { createApp } from "vue";
const app = createApp(App);
app.provide("api", new ApiClient());
app.mount("#app");Plugins use this pattern — app.use(MyPlugin) typically calls app.provide() internally for tokens consumers later inject.
When provide/inject vs Pinia vs props?
| Use when | |
|---|---|
| Props | parent → direct child; shape matters as a contract |
provide/inject |
ancestor → deep descendant, dependency-style (theme, locale, API client, auth user) |
| Pinia | shared state across many unrelated components, with a centralized API |
Rough heuristics:
- A theme, a router context, an API client, a localization function, a form context →
provide/inject. - A logged-in user store, a cart store, a list of notifications, a chat connection → Pinia (state with actions, dev tools, persistence).
- One-off data → props.
Avoid using provide/inject as a poor man’s global state — Pinia is built for that and gives you dev tools, hot module reload, and explicit boundaries.
Plugin pattern with provide.
// plugin.ts
import type { App, InjectionKey } from "vue";
export const ApiKey: InjectionKey<ApiClient> = Symbol("api");
export const apiPlugin = {
install(app: App, options: { baseUrl: string }) {
const client = new ApiClient(options.baseUrl);
app.provide(ApiKey, client);
},
};
// main.ts
app.use(apiPlugin, { baseUrl: "/api" });
// any component
const api = inject(ApiKey)!;
const users = await api.getUsers();This is how Vue Router, i18n, Apollo Client, and others expose themselves — app.use(...) wraps app.provide.
How does this compare to React Context?
| Vue | React | |
|---|---|---|
| Declare | provide(key, value) |
createContext(default) |
| Subscribe | inject(key) |
useContext(Ctx) |
| Provider component | implicit (any ancestor) | explicit <Ctx.Provider value=...> |
| Re-render on change | only consumers that read it (refs auto-track) | every consumer of the context |
| Typing | InjectionKey<T> |
Context<T> |
Vue’s “any ancestor can provide” is more flexible; React’s explicit Provider is more localized. Both can lead to “magic dependency” smell if overused.
Gotchas / edge cases
injectreturnsundefinedif no ancestor provided. Always handle (!if you guarantee it, or use a default).- String keys can collide across libraries — always prefer
Symbol/InjectionKeyfor shared code. provideis per-component-tree — if you mount two apps, each has its own provide tree.- Non-reactive providers — providing a plain object means descendants won’t react to mutations. Wrap in
ref/reactiveif you want reactivity. provideinsidesetuponly — can’t call frommountedor async callbacks (same hook rules).- Testing components that
inject— you need to mount with a parent that provides, or useglobal.providein@vue/test-utils. - Tree-shaking —
app.providealways runs at boot; large API clients should be lazy-instantiated.
What a senior is expected to say 5
- “
provide/injectis Vue’s Context — same use case (avoiding prop drilling for cross-cutting concerns), same caveat (overuse turns deps into magic).” - “
InjectionKey<T>(typed Symbol) for any cross-file or library provide — avoids string collisions and gives TS narrowing.” - “Provide refs (or wrapped objects) for reactivity; provide an exposed updater for write access, often with
readonly(ref)for read-only exposure.” - “Plugin pattern is
app.use(plugin)callingapp.provide— that’s how Vue Router and i18n expose themselves.” - “For shared state with actions and devtools, Pinia is the right tool, not provide/inject. Provide/inject for dependencies (theme, API client, locale).”
Cross-references
- Pinia for state management: Pinia (and Where Vuex Still Appears)
- Composition API: Composition API vs Options API (and Migration)
- React Context (analogue): React
Further reading
- Vue docs — Provide / Inject: https://vuejs.org/guide/components/provide-inject.html
- Vue docs — Plugins: https://vuejs.org/guide/reusability/plugins.html