Updated June 2026. Tested on Vue 3 with
<script setup>. Part of the Techalyst Vue 3 series.
Props pass data one level down. When a deeply nested grandchild needs something from a far-off ancestor, threading a prop through every component in between (prop drilling) gets tedious fast. provide and inject skip the chain: an ancestor provides a value, and any descendant, however deep, injects it directly.
The basic pair
In <script setup>, import provide and call it with a key and a value. Descendants call inject with the same key.
<!-- Ancestor.vue -->
<script setup>
import { provide } from 'vue'
provide('city', 'Colombo')
</script>
<!-- AnyDescendant.vue -->
<script setup>
import { inject } from 'vue'
const city = inject('city')
</script>
<template>
<p>{{ city }}</p>
</template>
Each provide call handles one key, so call it once per value you want to share. Provide a default in the descendant for when no ancestor supplied the key:
const city = inject('city', 'Unknown') // fallback if not provided
Keeping it reactive
A plain string like the example above is a one-time snapshot, change it later and nothing updates. To make the shared value reactive, provide a ref. Because a ref is an object, every descendant injects a reference to the same object, so a change anywhere propagates everywhere.
<!-- Ancestor.vue -->
<script setup>
import { ref, provide } from 'vue'
const city = ref('Colombo')
provide('city', city)
function move() {
city.value = 'Kandy' // updates every injecting descendant
}
</script>
<!-- Descendant.vue -->
<script setup>
import { inject } from 'vue'
const city = inject('city') // the same ref, auto-unwrapped in the template
</script>
<template>
<p>{{ city }}</p>
</template>
Refs injected this way are auto-unwrapped in the template, so no .value there.
Read-only versus two-way
Sharing a live ref means any descendant can write to it with city.value = ..., which can make data flow hard to follow. You usually want more control over who can change what.
For a value descendants should read but never write, provide a read-only computed. The getter keeps it in sync with the source, and writes are blocked.
<script setup>
import { ref, computed, provide } from 'vue'
const message = ref('Hello')
provide('message', computed(() => message.value)) // read-only downstream
</script>
When you do want descendants to update the source, the cleaner pattern is to provide the value alongside a function that mutates it. This keeps every write going through one explicit place in the ancestor.
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('count', readonly(count)) // descendants read only
provide('increment', () => { count.value++ }) // and ask for changes via this
</script>
<!-- Descendant.vue -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const increment = inject('increment')
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
That gives you the convenience of shared state without losing track of where mutations happen.
Symbol keys for larger apps
String keys can collide across a big codebase or a component library. Export a Symbol (or a unique string constant) from a shared file and use that as the key instead.
// keys.js
export const cityKey = Symbol('city')
// ancestor
import { cityKey } from './keys'
provide(cityKey, city)
// descendant
import { cityKey } from './keys'
const city = inject(cityKey)
When to reach for it
provide and inject shine for app-wide or subtree-wide context: a theme, the current user, a locale, or configuration that many components need. It is not a replacement for full state management, though. When several unrelated parts of the app share and mutate the same complex state, a dedicated store like Pinia is the better fit. Keep provide/inject for context that naturally flows down a branch of the tree.
Wrapping up
provide and inject cut out prop drilling by letting any descendant reach an ancestor's data directly. Provide a ref to keep it reactive, wrap it in readonly or a computed getter when descendants should only read, and pair it with an explicit updater function when they need to change it. Use Symbol keys to avoid collisions, and remember it complements rather than replaces a real store.
More in the series: props and emits and state management with Pinia. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.