Skip to the solution
MediumFrontend10 min read

What are provide and inject?

937 views

The problem

Question presented to candidate: "We pass the current theme and the signed-in user through six layers of components, and most of those layers never use them. What are provide and inject, how would they fix this, and what would you watch out for?"

What a strong answer should cover:

  • provide(key, value) in an ancestor makes a value available to every descendant; inject(key) reads it, however deep, without the components in between knowing.
  • The nearest provider wins: a provider lower in the tree shadows the same key for its own subtree, which is how nested themes or forms work.
  • Reactivity is not automatic: provide a ref, reactive object or computed to keep consumers in sync; a plain value is a one-time snapshot. Wrap with readonly() and provide an update function so only the owner mutates.
  • inject takes a default (inject("locale", "en-GB")), or a factory with the third argument true; with no provider and no default it returns undefined and warns.
  • inject only works synchronously inside setup() (or <script setup>); app.provide supplies app-wide values, and app.runWithContext() lets code outside components inject.
  • When to choose it over props (deep, subtree-scoped context) and over Pinia (app-global state), and using Symbol keys to avoid collisions.

Clarifying questions expected:

  • "Is the theme global to the app, or can a section of the page override it?"
  • "Who is allowed to change the user: any component, or only the auth layer?"

Code / implementation expected: Yes β€” a provider with a reactive value and an injecting descendant, ideally with a default and a readonly guard.

  • provide
  • inject

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

VueReactive provide, a shadowing provider, readonly refs and inject defaults
<script setup>
import { ref, readonly, provide, inject, defineComponent, h } from "vue";

const Leaf = defineComponent({
  setup() {
    const theme = inject("theme");                       // nearest provider wins
    const user = inject("user");                          // readonly ref from the top
    const locale = inject("locale", "en-GB");             // default when nobody provides it
    const fmt = inject("formatter", () => new Intl.NumberFormat("en-GB"), true); // factory default
    const missing = inject("missing");                    // no provider, no default
    const tryWrite = () => { user.value = "hacker"; };
    return () =>
      h("p", { class: "leaf" }, [
        `theme=${theme.value} user=${user.value} locale=${locale} ${fmt.format(1234.5)} missing=${missing}`,
        h("button", { class: "write", onClick: tryWrite }, "write user"),
      ]);
  },
});

const Section = defineComponent({
  setup(_, { slots }) {
    provide("theme", ref("dark"));                        // shadows the app-level theme below it
    const ownTheme = inject("theme");                     // what does the provider itself see?
    return () => h("section", [`section sees theme=${ownTheme.value} | `, slots.default()]);
  },
});

const theme = ref("light");
const user = ref("ada");
provide("theme", theme);
provide("user", readonly(user));
</script>

<template>
  <Leaf />
  <Section><Leaf /></Section>
  <button class="toggle" @click="theme = theme === 'light' ? 'sepia' : 'light'">toggle theme</button>
  <button class="rename" @click="user = 'grace'">rename user</button>
</template>
app.provide, runWithContext, and inject called outside setup (Node + jsdom script)
// node q3-app-level.mjs  (vue 3.5.44 + jsdom; run with NODE_ENV=development for warnings)
import { JSDOM } from "jsdom";

const dom = new JSDOM('<div id="app"></div>');
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Element: dom.window.Element, SVGElement: dom.window.SVGElement });
const { createApp, h, inject, provide, hasInjectionContext, defineComponent, nextTick } = await import("vue");

let count = 0; // a plain number, not a ref

const Child = defineComponent({
  setup() {
    const c = inject("count");
    return () => h("i", String(c));
  },
});

const app = createApp({
  setup() {
    provide("count", count); // hands over the value 0, not a live binding
    setTimeout(() => {
      // Timers run outside setup, so there is no injection context any more.
      console.log("hasInjectionContext() in setTimeout:", hasInjectionContext());
      console.log("inject in setTimeout ->", inject("api"));
    });
    return () => h(Child);
  },
});

// App-level provide: every component in this app can inject it.
app.provide("api", "https://api.example.com");
app.mount("#app");

// Code outside components (a router guard, a store) can borrow the app context.
console.log("runWithContext ->", app.runWithContext(() => inject("api")));

count++;
await nextTick();
console.log("child shows", document.querySelector("#app").innerHTML, "after count++");
await new Promise((r) => setTimeout(r, 10));

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 32 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What is defineModel() in Vue 3.4+ and how does it replace manual prop + emit?

Continue