frontend / vuejs
Provide / inject
Explication
Ce que vous allez apprendre
- Comprendre le problème du "prop drilling" que
provide/injectvient résoudre - Fournir une donnée à tous les descendants d'un composant, quel que soit leur niveau de profondeur
- Protéger une donnée fournie contre une mutation incontrôlée par un descendant lointain
- Utiliser des
Symbolcomme clés d'injection pour éviter les collisions de noms - Encapsuler
provide/injectdans un composable réutilisable (pattern "plugin")
Dans quel contexte ?
Une application affiche un thème (clair/sombre) et les informations de l'utilisateur connecté, utilisés par des composants situés jusqu'à dix niveaux de profondeur dans l'arbre de composants (un bouton dans un menu, dans un panneau, dans une page). Faire transiter ces deux informations par des props à travers chaque niveau intermédiaire — même les composants qui n'en ont eux-mêmes pas besoin — serait extrêmement fastidieux et fragile au moindre changement de structure.
D'abord, le problème que ce mécanisme résout
Le "prop drilling" désigne cette situation où une donnée doit transiter par de nombreux composants intermédiaires qui n'en ont pas besoin eux-mêmes, juste pour l'acheminer à un descendant plus profond. provide/inject contourne complètement ce problème : un ancêtre "fournit" une donnée, et n'importe quel descendant peut "l'injecter" directement, sans que les composants entre les deux n'aient à s'en soucier.
PanneauProfond.vue dans l'exemple illustre exactement ça : il n'a besoin de rien relayer, alors même que WidgetTresProfond, son propre enfant, consomme des données fournies bien plus haut dans l'arbre.
Prérequis
Il faut avoir compris les slots (leçon précédente) : provide/inject est un mécanisme complémentaire, pour des données plutôt que du contenu HTML.
Ensuite, une précaution essentielle sur ce qu'on fournit
Fournir directement une ref mutable brute permettrait à N'IMPORTE QUEL descendant de la modifier n'importe comment, rendant impossible de savoir d'où vient un changement inattendu. La bonne pratique consiste à fournir la donnée en lecture seule (readonly(theme)) accompagnée d'une fonction dédiée (basculerTheme) pour la modifier de façon contrôlée.
| Élément fourni | Rôle |
|---|---|
readonly(theme) | Lecture seule pour tous les descendants |
basculerTheme | Seule façon autorisée de modifier la donnée |
Piège fréquent
Fournir une ref mutable sans protection encourage des mutations dispersées dans toute l'application, rendant le débogage d'un changement d'état presque impossible à tracer. Toujours combiner readonly() et une fonction de mutation explicite, exactement comme dans l'exemple du thème.
Il reste un risque de collision à éviter
Utiliser une simple chaîne de caractères comme clé d'injection (provide('theme', ...)) peut entrer en collision avec une clé identique utilisée par une bibliothèque tierce ou un autre module du projet. Un Symbol('theme') garantit une clé unique, impossible à dupliquer accidentellement ailleurs dans le code.
Astuce
Le deuxième argument d'inject(ThemeKey, valeurParDefaut) fournit une valeur de repli si aucun ancêtre n'a fait de provide() correspondant — utile pour éviter une erreur si un composant est utilisé isolément (par exemple dans un test) sans son contexte habituel.
Maintenant, encapsuler le pattern pour le rendre réutilisable
Regrouper provide et inject dans un couple de fonctions composables (fournirTheme()/useTheme()) centralise la définition de la clé d'injection et l'expose via une API simple, tout en gardant la possibilité de lancer une erreur explicite si useTheme() est appelé en dehors d'un contexte qui l'a fourni.
Maintenant que tu sais partager des données à travers l'arbre de composants, la prochaine leçon aborde un cas d'usage extrêmement fréquent de la réactivité : les formulaires et les subtilités du v-model appliqué à différents types de champs.
Commandes & code
Provide / inject
Injection de dépendance pour éviter le "prop drilling" à travers de nombreux niveaux de composants.
// src/keys.js — centraliser les clés d'injection avec des Symbol évite les collisions de nom
// et permet un typage précis avec InjectionKey en TypeScript.
export const ThemeKey = Symbol('theme')
export const UtilisateurKey = Symbol('utilisateur')<!-- App.vue (ancêtre) -->
<script setup>
import { ref, provide, readonly } from 'vue'
import { ThemeKey, UtilisateurKey } from './keys'
const theme = ref('clair')
function basculerTheme() {
theme.value = theme.value === 'clair' ? 'sombre' : 'clair'
}
// On fournit la donnée EN LECTURE SEULE + une fonction dédiée pour la modifier,
// plutôt qu'une ref mutable brute : ça évite qu'un descendant lointain la modifie n'importe comment.
provide(ThemeKey, { theme: readonly(theme), basculerTheme })
const utilisateurConnecte = ref({ id: 1, nom: 'Alice', role: 'admin' })
provide(UtilisateurKey, readonly(utilisateurConnecte))
</script>
<template>
<button @click="basculerTheme">Basculer thème ({{ theme }})</button>
<PanneauProfond />
</template><!-- PanneauProfond.vue -- N'A PAS BESOIN de relayer la prop, quel que soit le nombre de niveaux -->
<template>
<div><WidgetTresProfond /></div>
</template><!-- WidgetTresProfond.vue -- consomme directement, même à 10 niveaux de profondeur -->
<script setup>
import { inject } from 'vue'
import { ThemeKey, UtilisateurKey } from './keys'
// deuxième argument = valeur par défaut si aucun ancêtre n'a fait de provide()
const { theme, basculerTheme } = inject(ThemeKey, { theme: 'clair', basculerTheme: () => {} })
const utilisateur = inject(UtilisateurKey)
</script>
<template>
<div :class="`theme-${theme}`">
Connecté en tant que {{ utilisateur?.nom }}
<button @click="basculerTheme">Changer de thème</button>
</div>
</template>// Pattern "plugin" : encapsuler provide/inject dans un composable réutilisable
// composables/useTheme.js
import { inject, provide, ref, readonly } from 'vue'
const ThemeKey = Symbol('theme')
export function fournirTheme() {
const theme = ref('clair')
const basculerTheme = () => { theme.value = theme.value === 'clair' ? 'sombre' : 'clair' }
provide(ThemeKey, { theme: readonly(theme), basculerTheme })
}
export function useTheme() {
const contexte = inject(ThemeKey)
if (!contexte) {
throw new Error('useTheme() doit être appelé dans un descendant de composant appelant fournirTheme()')
}
return contexte
}Résumé
provide/injectévite de faire transiter une prop manuellement à travers 5, 10, 15 composants.- Toujours fournir en lecture seule (
readonly) + fonctions dédiées à la mutation, jamais une ref brute mutable. - Utiliser des
Symbolcomme clés d'injection pour éviter toute collision entre bibliothèques/modules. - Ne pas abuser de
provide/injectpour de l'état global applicatif : Pinia est plus adapté à grande échelle.
Exercices pratiques
Mission : un thème modifié depuis n'importe où
Objectif : Sécuriser un provide/inject de thème mutable et non typé, en le rendant en lecture seule et sans collision de clé.
Contexte
Dans une application, App.vue fait provide('theme', theme) où theme est une ref('clair') brute, sans fonction dédiée. Un widget profondément imbriqué fait const theme = inject('theme'); theme.value = 'sombre' directement pour changer le thème. Une bibliothèque tierce récemment ajoutée au projet utilise elle aussi la chaîne 'theme' comme clé de provide pour ses propres besoins internes, et le thème de l'application se met parfois à changer de façon totalement imprévisible.