| |

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 functionThe 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 optionThe 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 typeThe use case
The stringThe application code
The SymbolThe library code
The InjectionKey<T>The TypeScript

The Reactivity

The valueThe reactive
The refYes
The reactiveYes
The computedYes
The plain objectNo
The primitiveNo

The Readonly

The wrapperThe behavior
readonly(ref)The readonly ref
readonly(reactive)The readonly reactive
The no wrapperThe mutable

The Patterns

The patternThe provider
The themeprovide('theme', { theme, toggleTheme })
The authprovide('auth', { user, login, logout })
The i18napp.provide('i18n', i18n)
The shared stateprovide('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 pitfallThe whyThe fix
The inject returns undefinedThe no providerAdd the default
The value is not reactiveThe plain objectUse the ref or the reactive
The value is mutatedThe no readonlyWrap in the readonly
The inject is undefinedThe outside the setupCall inside the setup
The key collisionThe stringUse the Symbol
The default is ignoredThe provider existsCheck the provider
The plugin missingThe 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

ItemValue
The provideThe register the value
The injectThe read the value
The compositionprovide() / inject()
The optionsprovide() / inject: []
The reactiveThe ref, the reactive, the computed
The readonlyThe prevent the mutation
The defaultThe fallback
The SymbolThe collision-free key
The app.provide()The application-level
The pluginThe useX() composable

Key takeaways:

  • 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() and the inject() inside the setup. The options API uses the provide option and the inject option.
  • The provided value can be the reactive. The ref, the reactive, and the computed are 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 the readonly and 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 Symbol key avoids the collisions. The library code uses the Symbol, and the provider and the injector import the same instance.
  • The app.provide() makes the value available to every component. The plugins use the app.provide() and the useX() composables wrap the inject().
  • 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!