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.
Interview Q&A
Q: Basic example.
A:
<!-- 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.
Q: Typed injection — InjectionKey<T>.
A: 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
const theme = inject(ThemeKey); // Ref<"light" | "dark"> | undefined
Symbols also avoid string-key collisions. Use them for any library/shared-app provide.
Q: Default values for inject.
A:
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.
Q: Reactive provide — make a value updateable from descendants?
A: 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 });
Q: App-level provide.
A: 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.
Q: When provide/inject vs Pinia vs props?
A:
| 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.
Q: Plugin pattern with provide.
A:
// 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.
Q: How does this compare to React Context?
A:
| 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
- “
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: 10_pinia.md
- Composition API: 05_composition_vs_options.md
- React Context (analogue): ../05_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