Getting Started With Provide/Inject In Vue.js

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.

Abdel Awad

Abdel Awad

July 15, 2025

Getting Started with Provide/Inject in Vue.js

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.

How Provide/Inject works

The Provide/Inject API consists of two functions: provide and inject, both of which are imported from the vue package.

Provide

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.

Inject

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.

Prop Drilling

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.

Building that Accordion Component

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.

Passing multiple values

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 Symbols for keys

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);

Type Safety

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);

Requiring the Provider

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.

Guarding Mutations

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.

Conclusion

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.

More certificates.dev articles

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.

Looking for Certified Developers?

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.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)