Dependency injection in the Vue 3 Composition API lets ancestor components or plugins supply shared services, configuration, and state managers cleanly across an entire component tree.
While simple cases use plain string keys with provide() and inject(), large multi-team applications benefit from JavaScript Symbol() keys and readonly() state wrappers. Those safeguards prevent key name collisions and protect shared state from accidental mutations.
flowchart TD
Key["AuthServiceKey = Symbol('AuthService')"] --> Provider["App.vue: provide(Key, readonlyState)"]
Provider --> ConsumerA["Navbar.vue: inject(Key)"]
Provider --> ConsumerB["Dashboard.vue: inject(Key)"]
Symbol() Keys: Export symbols from a dedicated keys.js module so third-party plugins or unrelated features never clash on string names.readonly() Protection: Wrap provided reactive objects in readonly() so consumers can read reactive values without mutating them directly.readonly state so every state change flows through one controlled place.First, export a shared Symbol key from src/keys/authKeys.js.
export const AuthServiceKey = Symbol('AuthService');
Next, wrap authState with readonly() in App.vue and provide it with a logout action.
<script setup>
import { reactive, provide, readonly } from 'vue';
import { AuthServiceKey } from './keys/authKeys';
import UserDashboard from './components/UserDashboard.vue';
const authState = reactive({
user: { name: 'Alex', email: '[email protected]' },
isAuthenticated: true
});
function logout() {
authState.user = null;
authState.isAuthenticated = false;
}
provide(AuthServiceKey, {
state: readonly(authState),
logout
});
</script>
<template>
<main>
<UserDashboard />
</main>
</template>
Finally, consume AuthServiceKey inside UserDashboard.vue using inject().
<script setup>
import { inject } from 'vue';
import { AuthServiceKey } from '../keys/authKeys';
const authService = inject(AuthServiceKey, null);
</script>
<template>
<div class="user-panel">
<div v-if="authService?.state.isAuthenticated">
<h3>Welcome, {{ authService.state.user.name }}</h3>
<button @click="authService.logout">Log Out</button>
</div>
<p v-else>Please sign in to continue.</p>
</div>
</template>
Symbol('Auth') separately in two files produces two distinct symbols that never match. Always export one shared Symbol constant.readonly(): Letting any child component mutate injected properties makes state bugs hard to track across large trees.true as the third argument: inject(Key, () => createDefault(), true).Symbol() injection keys?Import InjectionKey from 'vue' and type the symbol: export const AuthKey: InjectionKey<AuthContext> = Symbol('Auth'). Both provide and inject then infer types automatically.
Yes. Calling provide(AuthServiceKey, mockAuth) in an intermediate component overrides that key only for its own descendants.
readonly() block the provider itself from updating state?No. The provider keeps a reference to the original authState and updates it normally; readonly(authState) only returns a read-only proxy view for consumers.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
You've completed this section! Take a quick 5-question quiz to check your understanding.