Vue.js 38 🟢 Deep Component Hierarchy Injection using provide() and inject()
Props flow one level at a time. A parent passes the value to the child, and the child passes it to the grandchild, and the grandchild passes it to the great-grandchild. Each intermediate component receives the prop it does not use and forwards it down. This is the prop drilling, and it becomes painful when the distance is long and the intermediate components are the generic layout components that should not know about the application’s data. The provide() and the `inject()“ pair solves the problem. A component anywhere in the tree can provide a value, and any descendant can inject it, without the intermediate components knowing anything about it. The provide and the inject are the dependency injection for the Vue component tree.
The mechanism is the component hierarchy, not the global. The provide() registers the value on the current component instance, and the inject() looks up the tree from the current component to the root, returning the first matching value. The lookup is the ancestor-based, and the intermediate components are the transparent. The provided value is available to every descendant, no matter how deep, and the component that provides it does not need to know which descendants will inject it.
The provide() has the two forms. The options API uses the provide option, which can be the object or the function returning the object. The composition API uses the provide() function inside the setup, with the inject() function to read it. The composition API is the recommended for the <script setup>, and it allows the provided value to be the reactive: the ref, the reactive, or the computed. The injected value is the same reactive object, so the mutations propagate.
The injection has the important rules. The inject() can have the default value, which is used when no provider is found. The injection key should be the Symbol in the library code, to avoid the name collisions. The provided value should not be mutated by the descendants, unless the provider also passes the update function. The readonly() wraps the value to prevent the accidental mutation. The provide and the inject are not the replacement for the props; they are the tool for the cross-cutting concerns, the theme, the i18n, the auth, and the shared state.
This chapter covers three areas. First, why the provide and the inject exist — the prop-drilling problem and the dependency injection. Second, the syntax and the reactivity — the provide() and the inject() in the composition API, the provide option in the options API, the default value, and the Symbol keys. Third, the patterns and the pitfalls — the theme provider, the auth provider, the shared state, the readonly, the update function, and the plugin integration. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the injection flow.
Key point: The provide() and the inject() implement the dependency injection for the Vue component tree. A component anywhere in the tree provides the value, and any descendant injects it, without the intermediate components knowing. The composition API uses the provide() function inside the setup and the inject() to read it, and the provided value can be the reactive. The inject() can have the default value, and the Symbol key avoids the collisions. The provide and the inject are the tool for the cross-cutting concerns, not the replacement for the props.
Why the provide and the inject exist
The prop-drilling problem. A value originates at the top of the tree and is needed at the bottom. The intermediate components — the layout, the wrapper, the container — receive the prop and forward it without using it. The prop pollutes the interfaces, and the change to the prop requires the change in every intermediate component. The provide and the inject remove the intermediate: the provider supplies the value, and the injector reads it, and the components between them are the unaffected.
The cross-cutting problem. Some concerns span the whole application: the theme, the locale, the current user, the feature flags. Every component might need them, and passing them through the props is the impractical. The provide and the inject make them available everywhere, and the component that needs them injects them. The pattern is the dependency injection, and the tree is the container.
The reactivity problem. The provided value can be the reactive: the ref, the reactive, or the computed. The descendant that injects it receives the same reactive object, and the mutations propagate. The provider can pass the state and the update function, and the descendants can read the state and call the update. The pattern is the shared state without the global store.
The plugin problem. The Vue plugins use the provide and the inject to expose the plugin API to the application. The app.provide('i18n', i18nInstance) makes the i18n available to every component, and the useI18n() composable wraps the inject(). The provide and the inject are the mechanism behind the useX() composables that the libraries expose.
The naming problem. The injection key is the string or the Symbol. The string is the simple, and the Symbol is the collision-free. The library code should use the Symbol, and the application code can use the string. The key is the contract between the provider and the injector.
The trade-off. The provide and the inject make the dependency implicit: the component’s props do not show what it injects. The component that injects the theme is the coupled to the provider, and the reuse outside the provider requires the injection. The trade-off is between the convenience of the global availability and the explicitness of the props.
a. The composition API syntax
The provide() and the inject() are imported from the vue and called inside the setup.
<!-- The provider -->
<script setup>
import { provide, ref } from 'vue';
const theme = ref('light');
provide('theme', theme);
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
</script>
The provide('theme', theme) registers the theme ref under the 'theme' key. The descendant injects it:
<!-- The consumer -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
// The theme is the ref, and the theme.value is the current value.
</script>
<template>
<div :class="`theme-${theme}`">
The current theme is {{ theme }}
</div>
</template>
The inject('theme') returns the provided ref, and the template unwraps the ref automatically. The provider can also pass the functions:
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide('theme', {
theme: readonly(theme),
toggleTheme,
});
</script>
The readonly(theme) wraps the ref so the descendants cannot mutate it directly, and the toggleTheme is the update function.
The default value. The inject() accepts the second argument as the default:
const theme = inject('theme', 'light');
The 'light' is returned when no provider exists. The default can be the value or the factory:
const theme = inject('theme', () => 'light');
The factory form is the inject() with the default factory, and it is the lazy.
The Symbol key. The library code uses the Symbol to avoid the collisions:
// keys.js
export const ThemeKey = Symbol('theme');
export const AuthKey = Symbol('auth');
The provider and the injector import the same Symbol:
import { ThemeKey } from './keys';
provide(ThemeKey, theme);
const theme = inject(ThemeKey);
The Symbol is the unique, and the two files import the same instance.
The app.provide(). The application-level provide:
const app = createApp(App);
app.provide('theme', 'light');
The app.provide() makes the value available to every component in the application, and the inject() reads it. The plugin uses the app.provide() to expose the API.
b. The options API syntax
The options API uses the provide option, and the inject option:
export default {
provide() {
return {
theme: this.theme,
toggleTheme: this.toggleTheme,
};
},
data() {
return { theme: 'light' };
},
methods: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
},
},
};
The provide option is the function that returns the object, and the this is the component instance. The injection:
export default {
inject: ['theme'],
mounted() {
console.log(this.theme);
},
};
The inject option is the array of the keys, and the injected values are available as the instance properties.
The inject with the default. The object form:
export default {
inject: {
theme: {
from: 'theme',
default: 'light',
},
},
};
The from is the key, and the default is the fallback.
The reactivity. The provided value in the options API is not the reactive by default. The computed or the ref from the composition API is the way to make it reactive:
import { ref, computed } from 'vue';
export default {
provide() {
return {
theme: computed(() => this.theme),
};
},
};
The computed is the reactive, and the injector receives the reactive.
c. The patterns and the pitfalls
The theme provider. The theme provider is the classic example.
<!-- ThemeProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide('theme', {
theme: readonly(theme),
toggleTheme,
});
</script>
<template>
<slot />
</template>
The consumer:
<script setup>
import { inject } from 'vue';
const { theme, toggleTheme } = inject('theme');
</script>
<template>
<div :class="`theme-${theme}`">
<button @click="toggleTheme">Toggle</button>
</div>
</template>
The auth provider. The auth provider exposes the user and the login/logout functions.
<!-- AuthProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
const user = ref(null);
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
user.value = await response.json();
}
function logout() {
user.value = null;
}
provide('auth', {
user: readonly(user),
login,
logout,
});
</script>
The shared state. The provide and the inject are the shared state without the global store.
<!-- CounterProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
provide('counter', {
count: readonly(count),
increment,
});
</script>
The readonly. The readonly() prevents the descendant mutation:
provide('state', readonly(state));
The descendant that tries to mutate the readonly gets the warning.
The update function. The provider passes the update function, and the descendant calls it:
provide('counter', {
count: readonly(count),
increment,
});
The increment is the function that mutates the count, and the descendant calls the increment().
The plugin integration. The app.provide() and the inject() are the mechanism behind the useX() composables.
// The plugin
const MyPlugin = {
install(app, options) {
const api = createApi(options);
app.provide('myApi', api);
},
};
// The composable
export function useMyApi() {
const api = inject('myApi');
if (!api) throw new Error('useMyApi requires MyPlugin');
return api;
}
The useMyApi() wraps the inject() and throws when the plugin is missing.
The inject in the wrong place. The inject() must be called inside the setup or the setup function, not in the event handler or the async callback.
// ❌ The wrong
function handleClick() {
const theme = inject('theme'); // the inject outside the setup
}
// ✅ The correct
const theme = inject('theme');
function handleClick() {
console.log(theme.value);
}
The non-reactive default. The default value is not the reactive, and the provider change does not update it.
// The default is the static.
const theme = inject('theme', 'light');
If the provider exists, the default is ignored, and the provided value is the reactive.
Complete Example Session
<!-- ============================================
PART 1: THE BASIC PROVIDE AND INJECT
============================================ -->
<!-- The provider -->
<script setup>
import { provide, ref } from 'vue';
const message = ref('Hello from the provider');
provide('message', message);
</script>
<!-- The consumer -->
<script setup>
import { inject } from 'vue';
const message = inject('message');
</script>
<template>
<p>{{ message }}</p>
</template>
<!-- ============================================
PART 2: THE REACTIVE VALUE
============================================ -->
<!-- The provider -->
<script setup>
import { provide, ref } from 'vue';
const count = ref(0);
provide('count', count);
</script>
<!-- The consumer -->
<script setup>
import { inject } from 'vue';
const count = inject('count');
</script>
<template>
<p>{{ count }}</p>
<button @click="count++">Increment</button>
</template>
<!-- ============================================
PART 3: THE READONLY
============================================ -->
<!-- The provider -->
<script setup>
import { provide, ref, readonly } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
provide('counter', {
count: readonly(count),
increment,
});
</script>
<!-- The consumer -->
<script setup>
import { inject } from 'vue';
const { count, increment } = inject('counter');
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</template>
<!-- ============================================
PART 4: THE DEFAULT VALUE
============================================ -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme', 'light');
</script>
<template>
<div :class="`theme-${theme}`">...</div>
</template>
<!-- ============================================
PART 5: THE SYMBOL KEY
============================================ -->
// keys.js
export const ThemeKey = Symbol('theme');
// The provider
import { ThemeKey } from './keys';
provide(ThemeKey, theme);
// The consumer
import { ThemeKey } from './keys';
const theme = inject(ThemeKey);
<!-- ============================================
PART 6: THE APP.PROVIDE
============================================ -->
const app = createApp(App);
app.provide('theme', 'light');
<!-- ============================================
PART 7: THE THEME PROVIDER
============================================ -->
<!-- ThemeProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide('theme', {
theme: readonly(theme),
toggleTheme,
});
</script>
<template>
<slot />
</template>
<!-- ============================================
PART 8: THE AUTH PROVIDER
============================================ -->
<!-- AuthProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
const user = ref(null);
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
user.value = await response.json();
}
function logout() {
user.value = null;
}
provide('auth', {
user: readonly(user),
login,
logout,
});
</script>
<template>
<slot />
</template>
<!-- ============================================
PART 9: THE PLUGIN INTEGRATION
============================================ -->
// The plugin
const MyPlugin = {
install(app, options) {
const api = createApi(options);
app.provide('myApi', api);
},
};
// The composable
export function useMyApi() {
const api = inject('myApi');
if (!api) throw new Error('useMyApi requires MyPlugin');
return api;
}
<!-- ============================================
PART 10: THE COMPLETE EXAMPLE
============================================ -->
<!-- The keys -->
// keys.js
export const ThemeKey = Symbol('theme');
export const AuthKey = Symbol('auth');
<!-- ThemeProvider.vue -->
<script setup>
import { provide, ref, readonly } from 'vue';
import { ThemeKey } from './keys';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide(ThemeKey, {
theme: readonly(theme),
toggleTheme,
});
</script>
<template>
<slot />
</template>
<!-- ThemeToggle.vue -->
<script setup>
import { inject } from 'vue';
import { ThemeKey } from './keys';
const { theme, toggleTheme } = inject(ThemeKey);
</script>
<template>
<button @click="toggleTheme">
The theme is {{ theme }}
</button>
</template>
<!-- App.vue -->
<script setup>
import ThemeProvider from './ThemeProvider.vue';
import ThemeToggle from './ThemeToggle.vue';
</script>
<template>
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
</template>
The ten parts show the basic provide and inject, the reactive value, the readonly, the default value, the Symbol key, the app.provide(), the theme provider, the auth provider, the plugin integration, and the complete example.
Quick Reference
The Composition API
| The function | The purpose |
|---|---|
provide(key, value) | The register the value |
inject(key) | The read the value |
inject(key, default) | The read with the default |
readonly(value) | The prevent the mutation |
app.provide(key, value) | The application-level |
The Options API
| The option | The purpose |
|---|---|
provide() | The function returning the object |
inject: ['key'] | The array of the keys |
inject: { key: { from, default } } | The object form |
The Key Types
| The type | The use case |
|---|---|
| The string | The application code |
The Symbol | The library code |
The InjectionKey<T> | The TypeScript |
The Reactivity
| The value | The reactive |
|---|---|
The ref | Yes |
The reactive | Yes |
The computed | Yes |
| The plain object | No |
| The primitive | No |
The Readonly
| The wrapper | The behavior |
|---|---|
readonly(ref) | The readonly ref |
readonly(reactive) | The readonly reactive |
| The no wrapper | The mutable |
The Patterns
| The pattern | The provider |
|---|---|
| The theme | provide('theme', { theme, toggleTheme }) |
| The auth | provide('auth', { user, login, logout }) |
| The i18n | app.provide('i18n', i18n) |
| The shared state | provide('counter', { count, increment }) |
Best Practices
✅ Do This:
<!-- Use the provide and the inject for the cross-cutting concerns -->
provide('theme', { theme, toggleTheme }); // ✅
<!-- Use the readonly for the state -->
provide('theme', { theme: readonly(theme), toggleTheme }); // ✅
<!-- Use the Symbol for the library keys -->
export const ThemeKey = Symbol('theme'); // ✅
<!-- Use the default value for the optional -->
const theme = inject('theme', 'light'); // ✅
<!-- Use the app.provide for the application-level -->
app.provide('theme', 'light'); // ✅
<!-- Use the composable wrapper -->
export function useTheme() { return inject('theme'); } // ✅
<!-- Use the update function for the mutations -->
provide('counter', { count: readonly(count), increment }); // ✅
❌ Don’t Do This:
// Don't mutate the injected value directly
const count = inject('count');
count.value++; // the provider's state is mutated // ❌
// Don't call the inject outside the setup
function handleClick() { const theme = inject('theme'); } // ❌
// Don't use the string key in the library code
provide('my-api', api); // the collision risk // ❌
// Don't forget the default
const theme = inject('theme'); // the undefined // ❌
// Don't expect the default to be reactive
const theme = inject('theme', 'light'); // the static // ❌
// Don't use the provide and the inject for the parent-child
provide('data', data); // the props are the simpler // ❌
// Don't forget the readonly
provide('state', state); // the mutation risk // ❌
Common Pitfalls
| The pitfall | The why | The fix |
|---|---|---|
| The inject returns undefined | The no provider | Add the default |
| The value is not reactive | The plain object | Use the ref or the reactive |
| The value is mutated | The no readonly | Wrap in the readonly |
| The inject is undefined | The outside the setup | Call inside the setup |
| The key collision | The string | Use the Symbol |
| The default is ignored | The provider exists | Check the provider |
| The plugin missing | The no app.use() | Install the plugin |
Real-World Examples
1. The Basic Provide
provide('message', 'Hello');
2. The Reactive
const count = ref(0);
provide('count', count);
3. The Readonly
provide('counter', { count: readonly(count), increment });
4. The Default
const theme = inject('theme', 'light');
5. The Symbol
export const ThemeKey = Symbol('theme');
6. The app.provide
app.provide('theme', 'light');
7. The Theme Provider
provide('theme', { theme: readonly(theme), toggleTheme });
8. The Auth Provider
provide('auth', { user: readonly(user), login, logout });
9. The Composable
export function useTheme() {
return inject('theme');
}
10. The Complete
provide(ThemeKey, { theme: readonly(theme), toggleTheme });
Visual
The Provide and Inject Flow
┌──────────────────────────────────────────────────────────────┐
│ THE PROVIDE AND INJECT FLOW │
│ │
│ The App: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ provide('theme', theme) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The Layout: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The no theme. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The Header: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The no theme. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The Button: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const theme = inject('theme') │ │
│ │ → the App's theme │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The Layout and the Header are the transparent. │
│ │
└──────────────────────────────────────────────────────────────┘
The Reactive Provide
┌──────────────────────────────────────────────────────────────┐
│ THE REACTIVE PROVIDE │
│ │
│ The provider: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const theme = ref('light'); │ │
│ │ provide('theme', theme); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ the same ref │
│ ▼ │
│ The consumer: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const theme = inject('theme'); │ │
│ │ → the same ref │ │
│ │ │ │
│ │ theme.value = 'dark'; │ │
│ │ → the provider's theme is updated │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The provider and the consumer share the same ref. │
│ │
└──────────────────────────────────────────────────────────────┘
The Readonly Pattern
┌──────────────────────────────────────────────────────────────┐
│ THE READONLY PATTERN │
│ │
│ The provider: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const count = ref(0); │ │
│ │ function increment() { count.value++; } │ │
│ │ │ │
│ │ provide('counter', { │ │
│ │ count: readonly(count), │ │
│ │ increment, │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The consumer: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const { count, increment } = inject('counter'); │ │
│ │ │ │
│ │ count.value++; // the WARNING: readonly │ │
│ │ increment(); // the OK: the update function │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The readonly prevents the mutation; the function is the │
│ controlled update. │
│ │
└──────────────────────────────────────────────────────────────┘
The Symbol Key
┌──────────────────────────────────────────────────────────────┐
│ THE SYMBOL KEY │
│ │
│ keys.js: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ export const ThemeKey = Symbol('theme'); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ├── the provider imports │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ import { ThemeKey } from './keys'; │ │ │
│ │ │ provide(ThemeKey, theme); │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │
│ └── the consumer imports │ │
│ ┌──────────────────────────────────────────────┐ │ │
│ │ import { ThemeKey } from './keys'; │ │ │
│ │ const theme = inject(ThemeKey); │ │ │
│ └──────────────────────────────────────────────┘ │ │
│ │
│ The same Symbol instance is the key. │
│ The collision is the impossible. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| The provide | The register the value |
| The inject | The read the value |
| The composition | provide() / inject() |
| The options | provide() / inject: [] |
| The reactive | The ref, the reactive, the computed |
| The readonly | The prevent the mutation |
| The default | The fallback |
The Symbol | The collision-free key |
The app.provide() | The application-level |
| The plugin | The useX() composable |
Key takeaways:
- The
provide()and theinject()implement the dependency injection for the Vue component tree. A component anywhere in the tree provides the value, and any descendant injects it, without the intermediate components knowing. - The composition API uses the
provide()and theinject()inside thesetup. The options API uses theprovideoption and theinjectoption. - The provided value can be the reactive. The
ref, thereactive, and thecomputedare the reactive, and the mutations propagate to the descendants that inject the same object. - The
readonly()prevents the accidental mutation. The provider wraps the state in thereadonlyand passes the update function for the controlled changes. - The
inject()can have the default value. The default is the fallback when no provider exists, and it can be the value or the factory. - The
Symbolkey avoids the collisions. The library code uses theSymbol, and the provider and the injector import the same instance. - The
app.provide()makes the value available to every component. The plugins use theapp.provide()and theuseX()composables wrap theinject(). - The provide and the inject are for the cross-cutting concerns. The theme, the auth, the i18n, and the shared state are the patterns. The props are the simpler for the parent-child.
Remember: The provide() and the inject() implement the dependency injection. The provider supplies the value, and the descendant injects it, and the intermediate components are the transparent. The value can be the reactive, and the readonly() protects it. The Symbol key avoids the collisions, and the app.provide() makes it global. The result is the cross-cutting concerns without the prop drilling, which is the foundation of the clean component tree.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!