Vue.js course Β· Module 7: Props & Events
Provide / Inject
In this lesson5
When NOVA LAB broadcasts a message to the entire colony, it doesn't send it through each module separately - it uses a broadcast system. In Vue this mechanism is called provide/inject - passing data through multiple levels without prop drilling.
Take the colour theme of the panels. Mission control sets it in the App component, and a button deep inside the sidebar needs it. With props alone, the theme would have to pass through Layout, Page and Sidebar, even though none of them uses it - each one merely hands it on. That is prop drilling: the intermediaries' code swells, and renaming the prop means fixes on every floor of the station.
The Prop Drilling Problem
Without provide/inject you have to pass props through every level, even when the intermediaries do not need them at all:
1App β Layout β Page β Sidebar β DeepChild
2(theme) (theme) (theme) (theme) (theme)With provide/inject the ancestor broadcasts the value once, and only the descendant that really needs it receives it:
1App (provide theme) β ... β DeepChild (inject theme)The intermediaries stay untouched: they do not declare a theme prop and do not even know that the signal passes through them.
Provide/Inject Basics
The provide(key, value) function called in an ancestor makes the value available to its entire subtree, and the inject(key) function in any descendant receives it. The key is a string or a Symbol, and the second argument of inject is a default value in case no ancestor broadcast anything. You import both functions from vue and call them synchronously in <script setup>:
1<!-- Provider (ancestor) -->
2<script setup>
3import { provide, ref } from 'vue'
4
5const theme = ref('dark')
6provide('theme', theme)
7provide('labName', 'NOVA LAB Alpha')
8</script>
9
10<!-- Consumer (any descendant) -->
11<script setup>
12import { inject } from 'vue'
13
14const theme = inject('theme')
15const labName = inject('labName')
16
17// With default value
18const mode = inject('mode', 'standard')
19</script>If a descendant asks for a key that nobody provided and gives no default value, it gets undefined and Vue prints a warning in the console. That is why mode has the default value 'standard' here, while the other two keys come from the ancestor.
Reactive Provide
provide passes the value literally. A plain string reaches the descendant as a string and will not show later changes. Pass ref() or reactive() to make the data reactive - the descendant then receives the same object and sees every change. The readonly function wraps it in a read-only version:
1<!-- Provider -->
2<script setup>
3import { provide, ref, readonly } from 'vue'
4
5const count = ref(0)
6
7// Readonly - descendant cannot modify
8provide('count', readonly(count))
9
10// Actions for modification
11provide('increment', () => count.value++)
12</script>
13
14<!-- Consumer -->
15<script setup>
16import { inject } from 'vue'
17
18const count = inject('count') // readonly ref
19const increment = inject('increment') // function
20</script>The descendant reads count and refreshes along with it, but an attempt to assign to it ends with a console warning, and the value does not change. Changes go through the provided increment function, so state mutation stays with the provider. In the template the ref unwraps itself, and in the script you read it through count.value.
App-level Provide
A global provide in main.ts makes a value available to every component of the application. The provide method belongs to the application object returned by createApp:
1const app = createApp(App)
2app.provide('apiUrl', 'https://mars-api.novalab.space')
3app.mount('#app')The call comes before app.mount, just like plugin registration. Plugins themselves rely on this mechanism, because they have no component of their own in which they could call provide.
Typed Keys (InjectionKey)
In a large application two teams can unknowingly use the same string key. The documentation then recommends Symbol keys exported from a single file. In TypeScript the InjectionKey type attaches the type of the transmitted value to the symbol. The Provider and Consumer fragments below come from two different components:
1import type { InjectionKey, Ref } from 'vue'
2
3export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
4
5// Provider
6provide(themeKey, ref('dark'))
7
8// Consumer - TypeScript knows it's Ref<string> | undefined
9const theme = inject(themeKey)The result of inject includes undefined in its type, because the compiler cannot be sure that some ancestor called provide. It is enough to pass a default value, for example inject(themeKey, ref('light')), and the type narrows to Ref<string>. On the other hand, provide(themeKey, 'dark') with a bare string is rejected already at compile time.
My advice: reserve provide and inject for data the whole subtree needs - the theme, the logged-in crew member, configuration. For parent-child relationships props remain clearer. Always keep mutations with the provider: expose state through readonly and changes through functions. In the editor below MissionControl sits in the middle and passes no props, yet DeepChild still receives the theme. You will develop this pattern in the lesson on advanced communication patterns.
Remember: provide is the station's broadcast transmitter, and inject is a receiver you can mount on any floor below it.
Code for this lesson: App.vue
1<script setup>
2import { provide, ref, readonly } from 'vue'
3import MissionControl from './MissionControl.vue'
4
5const theme = ref('dark')
6const missionMode = ref('exploration')
7
8// Provide reactive data (readonly)
9provide('theme', readonly(theme))
10provide('missionMode', readonly(missionMode))
11
12// Provide actions for modification
13provide('toggleTheme', () => {
14 theme.value = theme.value === 'dark' ? 'light' : 'dark'
15})
16provide('setMission', (mode) => {
17 missionMode.value = mode
18})
19</script>
20
21<template>
22 <div class="nova-lab" :class="theme">
23 <h1>Provide / Inject Demo</h1>
24 <p>Theme: {{ theme }} | Mission: {{ missionMode }}</p>
25
26 <button @click="theme = theme === 'dark' ? 'light' : 'dark'">
27 Toggle Theme
28 </button>
29
30 <MissionControl />
31 </div>
32</template>
33
34<style scoped>
35.nova-lab {
36 min-height: 100vh;
37 padding: 20px;
38 font-family: monospace;
39}
40.nova-lab.dark {
41 background: #0a0e27;
42 color: #00ff88;
43}
44.nova-lab.light {
45 background: #f0f4f8;
46 color: #1a1a2e;
47}
48h1 { margin-bottom: 15px; }
49button {
50 background: #00b4d8;
51 color: #0a0e27;
52 border: none;
53 padding: 8px 16px;
54 cursor: pointer;
55 margin: 10px 5px;
56 font-family: monospace;
57}
58</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is provide/inject used for in Vue?
2. How do you ensure reactivity in provide/inject?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps for using provide/inject:
- Code editor
Use provide/inject to pass a theme (dark/light) across the entire application.
- Click in order
Arrange the code for reactive provide:
- Code editor
Create a provider with reactive state marsWeather (ref) and an updateWeather method.