Cours Vue.js · Module 7: Props et événements

Provide / Inject

5 min de lecture
Dans cette leçon5

Quand le NOVA LAB diffuse un message à toute la colonie, il ne le transmet pas module par module - il utilise un système de diffusion (broadcast). Dans Vue, ce mécanisme s'appelle provide/inject - la transmission de données à travers plusieurs niveaux sans prop drilling.

Prenons le thème de couleur des panneaux. Le centre de contrôle le définit dans le composant App, et un bouton tout au fond de la barre latérale en a besoin. Avec les seules props, le thème devrait traverser Layout, Page et Sidebar, alors qu'aucun d'eux ne l'utilise - chacun se contente de le faire passer. C'est ça, le prop drilling : le code des intermédiaires gonfle, et renommer la prop impose des corrections à chaque étage de la station.

Le problème du prop drilling

Sans provide/inject, tu dois transmettre les props à travers chaque niveau, même quand les intermédiaires n'en ont pas du tout besoin :

1App → Layout → Page → Sidebar → DeepChild
2(theme)  (theme)  (theme)  (theme)  (theme)

Avec provide/inject, l'ancêtre diffuse la valeur une seule fois, et seul le descendant qui en a vraiment besoin la reçoit :

1App (provide theme) → ... → DeepChild (inject theme)

Les intermédiaires restent intacts : ils ne déclarent pas de prop theme et ne savent même pas que le signal passe par eux.

Les bases de provide/inject

La fonction provide(clé, valeur) appelée dans un ancêtre met la valeur à disposition de tout son sous-arbre, et la fonction inject(clé) la reçoit dans n'importe quel descendant. La clé est une chaîne ou un Symbol, et le second argument de inject est une valeur par défaut au cas où aucun ancêtre n'aurait rien diffusé. Tu importes les deux fonctions depuis vue et tu les appelles de façon synchrone dans <script setup> :

1<!-- Fournisseur (ancêtre) -->
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<!-- Consommateur (n'importe quel descendant) -->
11<script setup>
12import { inject } from 'vue'
13
14const theme = inject('theme')
15const labName = inject('labName')
16
17// Avec valeur par défaut
18const mode = inject('mode', 'standard')
19</script>

Si un descendant demande une clé que personne n'a fournie et ne donne pas de valeur par défaut, il obtient undefined et Vue affiche un avertissement dans la console. C'est pourquoi mode a ici la valeur par défaut 'standard', tandis que les deux autres clés viennent de l'ancêtre.

Provide réactif

provide transmet la valeur telle quelle. Une simple chaîne arrive chez le descendant sous forme de chaîne et ne reflétera pas les changements ultérieurs. Transmets un ref() ou un reactive() pour que les données soient réactives - le descendant reçoit alors le même objet et voit chaque changement. La fonction readonly l'enveloppe dans une version en lecture seule :

1<!-- Fournisseur -->
2<script setup>
3import { provide, ref, readonly } from 'vue'
4
5const count = ref(0)
6
7// Readonly - le descendant ne peut pas modifier
8provide('count', readonly(count))
9
10// Actions pour la modification
11provide('increment', () => count.value++)
12</script>
13
14<!-- Consommateur -->
15<script setup>
16import { inject } from 'vue'
17
18const count = inject('count')       // ref readonly
19const increment = inject('increment') // fonction
20</script>

Le descendant lit count et se rafraîchit avec lui, mais une tentative d'affectation se termine par un avertissement dans la console, et la valeur ne change pas. Les modifications passent par la fonction fournie increment, donc la mutation de l'état reste chez le fournisseur. Dans le template, le ref se déballe tout seul, et dans le script tu le lis via count.value.

Provide au niveau de l'application

Un provide global dans main.ts met une valeur à disposition de tous les composants de l'application. La méthode provide appartient à l'objet application renvoyé par createApp :

1const app = createApp(App)
2app.provide('apiUrl', 'https://mars-api.novalab.space')
3app.mount('#app')

L'appel se place avant app.mount, comme l'enregistrement des plugins. Les plugins eux-mêmes s'appuient d'ailleurs sur ce mécanisme, car ils n'ont pas de composant à eux dans lequel appeler provide.

Clés typées (InjectionKey)

Dans une grande application, deux équipes peuvent utiliser sans le savoir la même clé textuelle. La documentation recommande alors des clés de type Symbol, exportées depuis un seul fichier. En TypeScript, le type InjectionKey associe au symbole le type de la valeur transmise. Les fragments Fournisseur et Consommateur ci-dessous proviennent de deux composants différents :

1import type { InjectionKey, Ref } from 'vue'
2
3export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
4
5// Fournisseur
6provide(themeKey, ref('dark'))
7
8// Consommateur - TypeScript sait que c'est Ref<string> | undefined
9const theme = inject(themeKey)

Le résultat de inject inclut undefined dans son type, car le compilateur ne peut pas être sûr qu'un ancêtre a appelé provide. Il suffit de donner une valeur par défaut, par exemple inject(themeKey, ref('light')), et le type se réduit à Ref<string>. À l'inverse, provide(themeKey, 'dark') avec une simple chaîne est rejeté dès la compilation.

Mon conseil : réserve provide et inject aux données dont tout le sous-arbre a besoin - le thème, le membre d'équipage connecté, la configuration. Pour une relation parent-enfant, les props restent plus lisibles. Garde toujours les mutations chez le fournisseur : expose l'état via readonly et les modifications via des fonctions. Dans l'éditeur ci-dessous, MissionControl se trouve au milieu et ne transmet aucune prop, et pourtant DeepChild reçoit bien le thème. Tu développeras ce motif dans la leçon sur les motifs de communication avancés.

Retiens : provide est l'émetteur de diffusion de la station, et inject un récepteur que tu peux installer à n'importe quel étage en dessous.

Code de cette leçon : 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 de données réactives (readonly)
9provide('theme', readonly(theme))
10provide('missionMode', readonly(missionMode))
11
12// Provide d'actions pour la 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>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert provide/inject dans Vue ?

  2. 2. Comment garantir la réactivité dans provide/inject ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les étapes pour utiliser provide/inject :

  • Éditeur de code

    Utilise provide/inject pour transmettre un thème (dark/light) à toute l'application.

  • Clique dans l'ordre

    Ordonne le code pour un provide réactif :

  • Éditeur de code

    Crée un fournisseur avec un état réactif marsWeather (ref) et une méthode updateWeather.