Explore what’s beyond props and events with Vue's powerful Provide/Inject API. This article teaches you how to escape prop drilling and build cleaner, more maintainable components with type safety, and advanced patterns.
Abdel Awad
July 15, 2025
The Vue.js Provide and Inject API is one of the most powerful features of Vue.js that brings you to the next level of component design. Many libraries in the ecosystem use this API to provide what seems like “magic” to the user.
One example is a tab component, usually you come across a couple of tab components that look like this:
<Tab>
<TabPanel title="Tab 1">
<p>Tab 1 content</p>
</TabPanel>
<TabPanel title="Tab 2">
<p>Tab 2 content</p>
</TabPanel>
<TabPanel title="Tab 3">
<p>Tab 3 content</p>
</TabPanel>
</Tab>
Another similar example is an accordion component that manages a set of accordion items:
<Accordion>
<AccordionItem title="Accordion 1">
<p>Accordion 1 content</p>
</AccordionItem>
<AccordionItem title="Accordion 2">
<p>Accordion 2 content</p>
</AccordionItem>
<AccordionItem title="Accordion 3">
<p>Accordion 3 content</p>
</AccordionItem>
</Accordion>
Looking at these examples, you might be wondering how do the Tab
or Accordion
components know about their children, and how do they communicate with each other? The only relationship we can see is that the parent component contains the child components.
The Provide/Inject
API consists of two functions: provide
and inject
, both of which are imported from the vue
package.
Components that call provide
are considered “providers”, and they can provide any value to the provider component’s descendants, regardless of the depth of the component tree.
Since there can be many providers in a component tree, each provided value is identified by a unique key.
import { provide } from 'vue';
provide('key', 'value');
One thing to keep in mind is that the provide
function must be called within the component’s setup
function. Calling it asynchronously, for example in the onMounted
hook, in watchers, or in event handlers, will not work as expected.
Here are a few samples of how you can call provide
in a component.
import { provide } from 'vue'
export default {
setup() {
// ✅ This is fine, called within the setup function.
provide('key', 'value');
}
}
<script setup>
import { provide } from 'vue'
// ✅ This is fine, called within the setup block.
provide('key', 'value');
</script>
<script setup>
import { onMounted, watch, computed, provide, inject } from 'vue'
// ❌ This will not work, called in a lifecycle hook callback.
onMounted(() => {
provide('key', 'value');
});
watch(() => props.value, (value) => {
// ❌ This will not work, called in a watcher.
provide('key', value);
});
const computedValue = computed(() => {
// ❌ This may not work, called in a computed property.
return inject('key');
});
function handleClick() {
// ❌ This may not work, called in an event handler.
provide('key', 'value');
}
</script>
Now that you understand how to call provide
, let’s see how to inject values in a component.
To receive a value from a provider, a descendant component uses the inject
function. It takes the key of the desired value as an argument and returns the associated value.
import { inject } from 'vue'
inject('key'); // returns 'value'
If the key is not found, inject
will return undefined
and show a warning in the console.
inject('key') // returns undefined and warns
You can suppress this warning by passing a fallback value as the second argument. In this case, it will not warn if the key is not found.
// returns 'fallback value' if the key is not found
inject('key', 'fallback value');
With the basics of provide
and inject
out of the way, let’s see how they can be useful.
There is a good rule in component design that says “props down, events up”. This means that you should pass props down to child components and listen for events from child components to communicate with them.
One issue that you always need to contend with when building applications is the need to pass the same prop or value down to multiple components at various levels of nesting.
This can be tedious and forces you to flatten your component tree, which is not ideal if you want to build maintainable and reusable components.
Consider a theme
prop that you need to pass down to every component in your design system.
This means you need to make sure that the theme
prop is passed to every component that needs it, and also to components that don’t need it but still need to pass it down to their children that might.
Now you can see why this is called “prop drilling”—you are effectively “drilling” the theme
prop down through your component tree.
This is where provide
and inject
come in. You could provide the theme
value at the root level, and then any component in your application can inject it as needed without having to declare it as a prop.
<script setup>
import { provide } from 'vue'
provide('theme', 'dark');
</script>
Then you can inject the theme
value in any component in your application.
<script setup>
import { inject } from 'vue'
const theme = inject('theme');
</script>
This works great for cases where you don’t need two-way communication between components, but what if you do? How can a child component at an unknown depth of the component tree communicate with the provider component?
Earlier I mentioned that you can provide any value you want to child components, and that includes reactive values.
Let’s build the accordion component we saw at the beginning of this article.
We could say that the Accordion
component needs to know only one thing: which accordion is selected!
But what about the child AccordionItem
components? Each AccordionItem
needs to know if it is selected to hide/show its content. They can determine this if they all have access to the currently selected item’s identifier.
That means we only have one value to pass around: the selected accordion item. We could represent it by an id, but for simplicity, we’ll use the title
of each item as a unique identifier.
First, let’s create the Accordion
component. I will leave the styling up to you, but this is the simplest possible structure for an accordion component:
<script setup>
import { provide, ref } from 'vue'
const selected = ref('');
provide('selected', selected);
</script>
<template>
<div>
<slot />
</div>
</template>
Now let’s create the AccordionItem
component:
<script setup>
import { inject } from 'vue'
const props = defineProps({
title: {
required: true,
type: String,
},
});
const selected = inject('selected');
function onClick() {
// set the selected value to its own title.
selected.value = props.title;
}
</script>
<template>
<div>
<div @click="onClick">{{ title }}</div>
<!-- Hide/show the content if own title matches the selected value -->
<div v-if="title === selected">
<slot />
</div>
</div>
</template>
It is a bit more involved, but nothing fancy. We check if the injected selected
value matches the title
prop passed to the AccordionItem
component, and if it does, we show the content. Otherwise, we hide it.
That’s it! We’ve now created an accordion component that can be used in our application and all it took is providing a reactive value to the component tree.
You can see it in action here, I added some styling to make things a bit cleaner.
Let’s go over a few caveats and tips before we wrap up.
What if we want to provide multiple values to the child components, like maybe the auth information for the user?
We could pass anything we want, right? So ask yourself, how do we normally return multiple values from a function? By returning an object, or an array.
So we could package the auth information in an object and provide it to the child components.
<script setup>
import { provide, ref, computed } from 'vue';
const user = ref({
name: 'John Doe',
email: 'john.doe@example.com',
authToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
});
const isAuthenticated = computed(() => !!user.value?.authToken);
provide('auth', {
user,
isAuthenticated,
});
</script>
<script setup>
import { inject } from 'vue';
const { user, isAuthenticated } = inject('auth');
</script>
Using plain strings as keys can lead to collisions in large applications where the same key might be used for different provided values.
One way to avoid this is to use ES6 Symbols as keys, as they are guaranteed to be unique.
const key1 = Symbol('key');
const key2 = Symbol('key');
key1 === key2 // false
provide(key1, 'value1');
provide(key2, 'value2');
inject(key1); // returns 'value1'
inject(key2); // returns 'value2'
Using this in practice is a bit more involved, since symbols are always unique, you have to export them to be able to provide/inject them.
For example you could maintain a injectionKeys.js
file that exports all the symbols you use in your application.
export const VALUE_KEY = Symbol('selectedAccordionItem');
Then you can import it and provide/inject the value.
// In provider component
import { provide } from 'vue';
import { VALUE_KEY } from './injectionKeys';
provide(VALUE_KEY, 'example');
// In child component
import { inject } from 'vue';
import { VALUE_KEY } from './injectionKeys';
inject(VALUE_KEY);
Provide/inject aren’t new in Vue 3, they have been around since Vue 2. But one cool aspect of them in Vue 3 is that you can have type safety when using them.
You can use the InjectionKey
type to type the keys you use in your application. The InjectionKey
type is a generic type helper that takes a type parameter that represents the type of the value you provide or inject.
In this example we are using a symbol as the key and we are typing the value as a string.
import type { InjectionKey } from 'vue';
export const VALUE_KEY: InjectionKey<string> = Symbol('key');
Now when providing the value for the selectedAccordionItem
key, you can only provide a string value.
// ✅ Works.
provide(VALUE_KEY, 'example');
// ❌ Does not work, not a string.
provide(VALUE_KEY, 123);
And when injecting the value, we know we are getting a string value. But since injections are optional, it can also be undefined
.
// string | undefined
const selected = inject(VALUE_KEY);
The type safety extends to the fallback value, you must have a fallback value of the same type as the value you are injecting.
// ✅ Works, now the type is `string`.
inject(VALUE_KEY, 'example');
// ❌ Does not work, not a string.
inject(VALUE_KEY, 123);
Sometimes, a component simply won’t work without its provider. For example, an <AccordionItem />
must be a child of an <Accordion />
component to function correctly.
<Accordion>
<!-- ... -->
</Accordion>
<!-- Oops, we are not a child of the Accordion component -->
<AccordionItem title="Accordion 1">
<p>Accordion 1 content</p>
</AccordionItem>
This can be critical for developer experience, because these issues only show up at runtime so they might be harder to catch and even then they might not be obvious.
To solve this, you can throw an error explicitly when the provider is not found.
import { inject } from 'vue';
const selected = inject('selected');
if (!selected) {
throw new Error('Selected value not found');
}
This ensures that you cannot use the AccordionItem
component outside of the Accordion
component.
I even like to have a utility function that is a stricter version of inject
that throws an error if the provider is not found.
import { inject } from 'vue';
const NOT_FOUND = Symbol('NOT_FOUND');
export function injectStrict(key) {
const value = inject(key, NOT_FOUND);
if (value === NOT_FOUND) {
throw new Error(`No provider found for key "${key.toString()}"`);
}
return value;
}
You will notice that I used a symbol as a fallback, this is because I want to silence the warning that is shown when the provider is not found (because we will throw anyways), and I want to allow injecting falsy values, so if the provider is not found, the inject
function will return the symbol which we check for.
Exposing mutable reactive state to any descendant component can be risky. You might want to control how the state is mutated, perhaps for validation or to manage complex state transitions.
One example here is the auth
information for the signed-in user. While you want to expose it to child components, you also want to prevent it from being mutated by accident.
We can use the readonly
function from vue
to make the provided value read-only. We can also provide functions that mutate the state in a safe, controlled way.
import { provide, readonly, ref } from 'vue';
const user = ref({
name: 'John Doe',
email: 'john.doe@example.com',
authToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
});
function signOut() {
user.value = null;
}
provide('user', {
user: readonly(user),
signOut,
});
Now no component can directly mutate the user
object. Instead, components must “ask” the provider to sign out the user by calling the signOut
function.
There are more things that you can do with provide/inject, but we’ll leave it at that for now.
Provide/inject is a powerful feature that can help you build more maintainable and reusable components. It is used by many libraries in the Vue ecosystem and is a great way to build components that are more flexible and easier to reuse.
In our senior Vue.js certification program, you will encounter a few questions and exercises that require you to use provide/inject, so make sure to practice it. I hope this article has given you a good overview of how to use it.
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.
Getting Started With Provide/Inject In Vue.js
Explore what’s beyond props and events with Vue's powerful Provide/Inject API. This article teaches you how to escape prop drilling and build cleaner, more maintainable components with type safety, and advanced patterns.
July 15, 2025
Abdel Awad
Event Delegation: One Listener to Rule Them All
Learn how event delegation in JavaScript lets you use a single event listener to handle clicks on multiple child elements efficiently—ideal for dynamic UIs and better performance.
July 14, 2025
Martin Ferret
Nuxt 4 is coming out, what does it mean?
Nuxt 4 (alpha) launched in June 2025 with a stable release on the horizon — bringing a smooth, stress-free upgrade path from Nuxt 3. Unlike past painful rewrites, Nuxt 4 builds on a solid foundation with improved developer experience, and it’s worth migrating now before Nuxt 5 arrives.
July 9, 2025
Reza Baar
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.