When a parent component needs to pass data to a deeply nested child, you'd normally have to pass props through every component in between — even if intermediate components don't use the data. This is called "prop drilling," and it makes your code fragile and hard to maintain.
provide and inject solve this. A parent provides data, and any descendant — no matter how deep — can inject it directly without any intermediate component knowing about it.
How it works
<!-- GrandParent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script><!-- DeeplyNestedChild.vue (any depth below GrandParent) -->
<script setup lang="ts">
import { inject } from 'vue'
const theme = inject('theme') // 'dark' — no props passed through middle components
</script>The provided value is reactive. When theme changes in the parent, every component that injected it updates automatically.
Type-safe injection with InjectionKey
String keys work but don't give you type safety. Use InjectionKey for typed provide/inject:
// keys.ts
import type { InjectionKey, Ref } from 'vue'
interface User { name: string; role: string }
export const UserKey: InjectionKey<Ref<User>> = Symbol('user')// Provider
import { provide, ref } from 'vue'
import { UserKey } from '@/keys'
const user = ref<User>({ name: 'John', role: 'admin' })
provide(UserKey, user)// Consumer
import { inject } from 'vue'
import { UserKey } from '@/keys'
const user = inject(UserKey) // Ref<User> | undefined
const user = inject(UserKey, ref({ name: 'Guest', role: 'viewer' })) // with defaultWhen to use it (and when not to)
Good use cases:
- Theme or locale shared across an entire app
- Auth/user state accessible deep in the tree
- Table or form context (a
<Table>provides column config, child<TableCell>injects it) - Plugin-style features (a toast manager, a modal manager)
Bad use cases:
- Passing data between siblings — provide/inject is parent-to-descendant only
- Replacing all props with inject — makes components harder to test and understand because their dependencies are implicit
- Global state that many unrelated components read and write — use Pinia instead
Provide/Inject vs Props vs Pinia
| Props | Provide/Inject | Pinia | |
|---|---|---|---|
| Direction | Parent → child (1 level) | Ancestor → any descendant | Any component → any component |
| Explicit | Yes (visible in template) | No (implicit dependency) | Somewhat (import store) |
| Reactive | Yes | Yes | Yes |
| Best for | Direct parent-child data | Subtree-wide context | Global app state |
See also: What is getCurrentInstance() and why should you avoid it? · What is a composable?
References
- Provide / Inject - Vue.js docs
- provide() - Vue.js docs
- inject() - Vue.js docs