Vue – provide and inject

November 23, 20253 min readUpdated 8/24/2026

Props pass data one level. When a value is needed five levels down, passing it through every component in between is called prop drilling, and every one of those intermediate components has to declare a prop it does not use.

provide and inject skip the middle. An ancestor provides a value; any descendant, at any depth, injects it.

The basics

// Ancestor
import { provide, ref } from "vue";

const sidebarOpen = ref(false);
provide("sidebarOpen", sidebarOpen);
// Any descendant, at any depth
import { inject } from "vue";

const sidebarOpen = inject("sidebarOpen");

Nothing in between mentions it. Provide a ref, not ref.value — the same rule as everywhere else, and the same silent failure if you get it wrong.

Both must be called at the top level of setup. inject resolves by walking up the component tree at setup time, so there is nothing to walk later.

Injection keys

A string key is a global namespace with no collision detection. Two libraries both providing "theme" will silently shadow each other. Use a Symbol:

// keys.js
export const SIDEBAR = Symbol("sidebar");
import { SIDEBAR } from "../keys";

provide(SIDEBAR, { open: sidebarOpen, toggle });
const sidebar = inject(SIDEBAR);

A Symbol cannot collide, and it gives you something to import — so "who provides this?" is answerable by following a reference rather than grepping for a string.

Defaults, and failing loudly

If nothing provided the key, inject returns undefined and warns. A second argument supplies a default instead:

const sidebar = inject(SIDEBAR, null);                   // optional
const theme = inject(THEME, () => createTheme(), true);  // factory, for expensive defaults

When a component genuinely cannot work without it, fail immediately rather than 200ms later with a TypeError on undefined:

const sidebar = inject(SIDEBAR);
if (!sidebar) {
  throw new Error("<AdminNavLink> must be used inside <AdminLayout>.");
}

That error names the actual problem. Worth the three lines.

Keep it read-only

Provided state is reactive, so a descendant can write to it. Nothing stops sidebarOpen.value = true from a component six levels down — and then the reason the sidebar opened is somewhere in a tree you now have to search.

Provide the value readonly and a function to change it:

import { provide, readonly, ref } from "vue";

const sidebarOpen = ref(false);

function toggleSidebar() {
  sidebarOpen.value = !sidebarOpen.value;
}

// Descendants can read the state and request a change. They cannot assign to it.
provide(SIDEBAR, {
  open: readonly(sidebarOpen),
  toggle: toggleSidebar,
});

Same discipline as props down and events up, applied at a distance. Mutations go through a named function, so there is one place to put a breakpoint.

App-level provide

A value every component needs can be provided on the application instance:

const app = createApp(App);
app.provide("apiBase", import.meta.env.VITE_API_BASE);

This is what plugins do internally. It is how the router makes useRoute() work without a single component importing the router instance.

provide/inject versus Pinia

The question that actually matters, because they overlap.

Use provide/inject when the value belongs to a subtree. A form component providing its validation state to whatever fields are inside it. A layout providing its sidebar state. Two instances of the component should have two independent values — and they do, automatically, because provision follows the component tree.

Use a store when the value belongs to the application. The signed-in user. The toast queue. There is exactly one, it does not belong to any subtree, and you want it in devtools.

The demo application uses Pinia for both of its shared concerns and does not use provide/inject at all — with two stores and a shallow tree, there was never a subtree that owned something. That is common: reach for a store first, and use provide/inject when you notice you are building something whose state genuinely belongs to one instance of one component.

The other real use is library authoring. A tabs component providing the selected tab to its panels cannot use a global store, because a page might have three sets of tabs.

Next: State Management with Pinia.