Transmettre des données en profondeur avec le contexte
Habituellement, vous transmettez les informations d’un composant parent à un composant enfant via les props. Cependant, ça peut devenir verbeux et peu pratique si vous devez les faire passer à travers de nombreux composants intermédiaires, ou si plusieurs composants de votre appli ont besoin de la même information. Le contexte permet au composant parent de mettre à disposition certaines informations à n’importe quel composant de l’arbre situé en dessous de lui — peu importe la profondeur — sans avoir à les passer explicitement par le biais des props.
Vous allez apprendre
- Ce que signifie « faire percoler des props »
- Comment remplacer le passage répétitif des props par un contexte
- Les cas d’utilisation classiques du contexte
- Les alternatives courantes au contexte
Le problème du passage de props
Le passage de props est un excellent moyen d’acheminer explicitement des données à travers l’arborescence de l’interface utilisateur jusqu’au composant qui les utilise.
Cependant, passer des props peut devenir verbeux et peu pratique quand vous devez passer certaines props profondément dans l’arbre, ou si de nombreux composants nécessitent la même prop. L’ancêtre commun peut être très éloigné du composant qui nécessite cette donnée, et faire remonter l’état aussi loin peut amener à une situation que l’on appelle « la percolation des props » (“prop drilling”, NdT).
Ne serait-ce pas génial s’il existait une façon de « téléporter » la valeur jusqu’aux composants de l’arbre qui en ont besoin sans passer par les props ? Grâce à la fonctionnalité de contexte de React, c’est possible !
Le contexte : une alternative au passage de props
Le contexte permet à un composant parent de mettre des données à disposition de tout l’arbre en dessous de lui. Il existe de nombreuses utilisations pour un contexte. En voici un exemple. Prenez ce composant Heading
qui utilise level
pour déterminer son niveau :
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Titre</Heading> <Heading level={2}>Section</Heading> <Heading level={3}>Sous-section</Heading> <Heading level={4}>Sous-sous-section</Heading> <Heading level={5}>Sous-sous-sous-section</Heading> <Heading level={6}>Sous-sous-sous-sous-section</Heading> </Section> ); }
Error
Extra 185 of 186 byte(s) found at buffer[1]
Disons que vous voulez que tous les titres au sein de la même Section
aient le même niveau :
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Titre</Heading> <Section> <Heading level={2}>Section</Heading> <Heading level={2}>Section</Heading> <Heading level={2}>Section</Heading> <Section> <Heading level={3}>Sous-section</Heading> <Heading level={3}>Sous-section</Heading> <Heading level={3}>Sous-section</Heading> <Section> <Heading level={4}>Sous-sous-section</Heading> <Heading level={4}>Sous-sous-section</Heading> <Heading level={4}>Sous-sous-section</Heading> </Section> </Section> </Section> </Section> ); }
Pour le moment, vous passez la prop level
individuellement à chaque <Heading>
:
<Section>
<Heading level={3}>À propos</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Vidéos</Heading>
</Section>
Il serait intéressant de pouvoir passer la prop level
au composant <Section>
et de la supprimer de <Heading>
. Ainsi, vous pourriez garantir que tous les titres d’une section ont le même niveau :
<Section level={3}>
<Heading>À propos</Heading>
<Heading>Photos</Heading>
<Heading>Vidéos</Heading>
</Section>
Mais comment le composant <Heading>
peut-il connaître le niveau de sa <Section>
la plus proche ? Il faudrait pour ça qu’un enfant puisse « demander » une donnée à un niveau supérieur de l’arbre.
Ce n’est pas possible uniquement avec les props. C’est ici que le contexte entre en jeu. Vous allez faire ça en trois étapes :
- Créez un contexte (vous pouvez l’appeler
LevelContext
, puisque c’est le niveau des en-têtes). - Utilisez ce contexte au niveau du composant qui a besoin de la donnée (
Heading
utiliseraLevelContext
). - Fournissez ce contexte depuis le composant qui spécifie la donnée (
Section
fourniraLevelContext
).
Les contextes permettent à un parent — aussi distant soit-il — de fournir des données à l’ensemble de l’arbre en dessous de lui.
Étape 1 : créer le contexte
Tout d’abord, il vous faut créer le contexte. Vous devrez l’exporter depuis un fichier pour que vos composants puissent l’utiliser :
import { createContext } from 'react'; export const LevelContext = createContext(1);
Le seul argument à createContext
est la valeur par défaut. Ici, 1
fait référence au niveau d’en-tête le plus élevé, mais vous pouvez passer n’importe quel type de valeur (y compris un objet). Vous verrez l’importance des valeurs par défaut dans la prochaine étape.
Étape 2 : utiliser le contexte
Importez le Hook useContext
depuis React, ainsi que votre contexte :
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Pour le moment, le composant Heading
lit level
depuis les props :
export default function Heading({ level, children }) {
// ...
}
Supprimez la prop level
, et lisez plutôt la valeur depuis le contexte LevelContext
que vous venez d’importer :
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
est un Hook. Tout comme useState
et useReducer
, vous ne pouvez appeler un Hook qu’au niveau racine d’un composant React (et non pas dans des boucles ou des conditions). useContext
indique à React que le composant Heading
souhaite lire le LevelContext
.
Maintenant que votre composant Heading
n’a plus besoin de la prop level
, vous n’avez plus besoin de la passer à Heading
dans votre JSX :
<Section>
<Heading level={4}>Sous-sous-section</Heading>
<Heading level={4}>Sous-sous-section</Heading>
<Heading level={4}>Sous-sous-section</Heading>
</Section>
Mettez à jour le JSX afin que Section
la reçoive désormais :
<Section level={4}>
<Heading>Sous-sous-section</Heading>
<Heading>Sous-sous-section</Heading>
<Heading>Sous-sous-section</Heading>
</Section>
Pour rappel, voici le code que vous essayez de faire marcher :
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Titre</Heading> <Section level={2}> <Heading>Section</Heading> <Heading>Section</Heading> <Heading>Section</Heading> <Section level={3}> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Section level={4}> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> </Section> </Section> </Section> </Section> ); }
Remarquez que cet exemple ne fonctionne pas encore tout à fait. Tous les en-têtes ont le même niveau parce que même si vous utilisez le contexte, vous ne l’avez pas encore fourni. React ne sait pas où l’obtenir.
Si vous ne fournissez pas le contexte, React utilisera la valeur par défaut que vous avez spécifiée dans l’étape précédente. Dans cet exemple, vous aviez spécifié 1
comme argument à createContext
, donc useContext(LevelContext)
renvoie 1
, transformant tous ces en-têtes en <h1>
. Corrigeons ce problème en demandant à chaque Section
de fournir son propre contexte.
Étape 3 : fournir le contexte
Le composant Section
effectue actuellement le rendu de ses enfants comme suit :
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Enrobez-les avec le fournisseur de contexte pour qu’ils accèdent à LevelContext
:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
React le comprend ainsi : « si un composant à l’intérieur de <Section>
demande LevelContext
, alors donne-lui ce level
». Le composant utilisera la valeur du <LevelContext.Provider>
le plus proche dans l’arbre au-dessus de lui.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Titre</Heading> <Section level={2}> <Heading>Section</Heading> <Heading>Section</Heading> <Heading>Section</Heading> <Section level={3}> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Section level={4}> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> </Section> </Section> </Section> </Section> ); }
Le résultat est le même qu’avec le code d’origine, mais vous n’avez plus besoin de transmettre la prop level
à chaque composant Heading
. Au lieu de ça, il « détermine » son niveau d’en-tête en interrogeant la Section
la plus proche :
- Vous passez une prop
level
à la<Section>
. Section
enrobe ses enfants dans un<LevelContext.Provider value={level}>
.Heading
demande la valeur la plus proche deLevelContext
avecuseContext(LevelContext)
.
Utiliser et fournir le contexte depuis le même composant
À ce stade, vous devez quand même spécifier manuellement le level
de chaque section :
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Puisque le contexte vous permet de lire une information à partir d’un composant plus haut, chaque Section
pourrait lire le level
de la Section
supérieure, puis transmettre automatiquement level + 1
en dessous d’elle. Voici comment faire :
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
Avec ce changement, vous n’avez plus besoin de passer la prop level
ni à <Section>
ni à <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Titre</Heading> <Section> <Heading>Section</Heading> <Heading>Section</Heading> <Heading>Section</Heading> <Section> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Heading>Sous-section</Heading> <Section> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> <Heading>Sous-sous-section</Heading> </Section> </Section> </Section> </Section> ); }
Désormais, Heading
et Section
lisent le LevelContext
pour déterminer à quelle « profondeur » ils se trouvent. Section
enrobe ses enfants dans un LevelContext
pour spécifier que tout son contenu se situe à un niveau « plus profond ».
Le contexte traverse les composants intermédiaires
Vous pouvez insérer autant de composants que vous le souhaitez entre le composant qui fournit le contexte et celui qui l’utilise. Ça inclut aussi bien les composants natifs comme <div>
que ceux que vous pourriez créer vous-même.
Dans cet exemple, le même composant Post
(avec une bordure en pointillés) est rendu à deux niveaux d’imbrication différents. Remarquez que le <Heading>
à l’intérieur obtient automatiquement son niveau depuis la <Section>
la plus proche :
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>Mon profil</Heading> <Post title="Bonjour voyageur !" body="Lisez mes aventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Billets</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Billets récents</Heading> <Post title="Les saveurs de Lisbonne" body="...et ses pastéis de nata !" /> <Post title="Buenos Aires au rythme du tango" body="J'ai adoré !" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Vous n’avez rien eu à faire pour que ça marche. Une Section
spécifie le contexte pour l’arbre qu’elle contient, vous pouvez donc y insérer un <Heading>
n’importe où et il aura le niveau correct. Essayez donc dans le bac à sable ci-dessus.
Le contexte vous permet d’écrire des composants qui « s’adaptent à leur environnement » et s’affichent différemment en fonction de l’endroit (autrement dit dans quel contexte) ils sont rendus.
La façon dont les contextes fonctionnent peut vous rappeler l’héritage des valeurs de propriétés en CSS. En CSS, vous pouvez spécifier color: blue
pour un <div>
et n’importe quel nœud du DOM qu’il contient, aussi profond soit-il, héritera de cette couleur, à moins qu’un nœud intermédiaire ne surcharge ça avec color: green
. C’est pareil avec React : la seule façon de surcharger un contexte venant d’en haut est d’enrober les enfants dans un fournisseur de contexte avec une valeur différente.
En CSS, des propriétés distinctes comme color
et background-color
ne sont pas remplacées les unes par les autres. Vous pouvez définir la color
de toutes les <div>
en rouge sans impacter la background-color
. De la même façon, des contextes React distincts ne s’écrasent pas les uns les autres. Chaque contexte que vous créez avec createContext()
est complétement isolé des autres et lie les composants qui utilisent et fournissent ce contexte particulier. Un composant peut utiliser et fournir différents contextes sans problème.
Avant d’utiliser un contexte
Il est souvent très tentant de recourir à des contextes ! Toutefois, il est aussi très facile d’en abuser. Ce n’est pas parce que vous devez propager des props sur plusieurs niveaux que vous devez mettre ces informations dans un contexte.
Voici certaines alternatives à considérer avant d’utiliser un contexte :
- Commencez par passer les props. Si vos composants ne sont pas triviaux, il est courant de passer une douzaine de props à travers une douzaine de composants. Ça peut sembler fastidieux, mais ça permet de savoir clairement quels composants utilisent quelles données ! La personne chargée de la maintenance de votre code sera ravie que vous ayez rendu le flux de données explicite grâce aux props.
- Extrayez des composants et passez-leur du JSX dans les
children
. Si vous passez des données à travers plusieurs couches de composants qui ne les utilisent pas (et ne font que les transmettre plus bas), ça signifie souvent que vous avez oublié d’extraire certains composants en cours de route. Par exemple, vous pouvez passer certaines données en props commeposts
à des composants visuels qui ne les utilisent pas directement, tel que<Layout posts={posts} />
. Préférez faire en sorte queLayout
prennechildren
en tant que prop, puis faites le rendu de<Layout><Posts posts={posts} /></Layout>
. Ça réduit le nombre de couches entre les composants qui spécifient la donnée et ceux qui l’utilisent.
Si aucune de ces approches ne vous convient, envisagez un contexte.
Cas d’utilisation des contextes
- Thème : si votre appli permet à l’utilisateur d’en changer l’apparence (comme le mode sombre), vous pouvez mettre un fournisseur de contexte tout en haut de votre appli et utiliser ce contexte dans les composants qui ont besoin d’ajuster leur aspect.
- Compte utilisateur : de nombreux composants peuvent avoir besoin de connaître l’utilisateur actuellement connecté. Le fait de le placer dans le contexte en facilite la lecture depuis n’importe quel endroit de l’arbre. Certaines applis vous permettent d’utiliser plusieurs comptes simultanément (par exemple pour laisser un commentaire avec un utilisateur différent). Dans ce cas, il peut être pratique d’enrober une partie de l’interface utilisateur dans un fournisseur avec un compte utilisateur différent.
- Routage : la plupart des solutions de routage utilise un contexte en interne pour conserver la route actuelle. C’est ainsi que chaque lien « sait » s’il est actif ou non. Si vous construisez votre propre routeur, vous serez peut-être tenté·e de faire de même.
- Gestion d’état : au fur et à mesure que votre appli grandit, vous pouvez vous retrouver avec de nombreux états proches de la racine de votre appli. De nombreux composants distants pourraient vouloir les changer. Il est courant d’utiliser un réducteur avec un contexte pour gérer des états complexes et les transmettre à des composants distants sans trop galérer.
Le contexte ne se limite pas aux valeurs statiques. Si vous passez une valeur différente au prochain rendu, React mettra à jour tous les composants descendants qui le lisent ! C’est pourquoi le contexte est souvent utilisé en combinaison avec l’état.
D’une manière générale, si certaines informations sont nécessaires pour des composants distants dans différentes parties de l’arborescence, alors c’est une bonne indication que le contexte vous sera utile.
En résumé
- Le contexte permet à un composant de fournir certaines informations à l’ensemble de l’arbre situé en dessous de lui.
- Pour transmettre un contexte :
- Créez-le et exportez-le avec
export const MyContext = createContext(defaultValue)
. - Passez-le au Hook
useContext(MyContext)
depuis n’importe quel composant enfant pour pouvoir le lire, aussi profondément imbriqué soit-il. - Enrobez les enfants dans un
<MyContext.Provider value={...}>
pour le fournir depuis un parent.
- Créez-le et exportez-le avec
- Le contexte traverse tous les composants intermédiaires.
- Le contexte vous permet d’écrire des composants qui « s’adaptent à leur environnement ».
- Avant d’utiliser un contexte, essayez de passer par les props ou en mettant du JSX dans les
children
.
Défi 1 sur 1 · Remplacer la percolation de props par un contexte
Dans cet exemple, le fait d’activer la case à cocher change la prop imageSize
passée à chaque <PlaceImage>
. L’état de cette case à cocher est conservé dans le composant racine App
, mais chaque <PlaceImage>
doit en être informé.
Pour l’instant, App
transmet imageSize
à List
, qui la transmet ensuite à chaque Place
qui transmettent enfin à PlaceImage
. Supprimez la prop imageSize
pour la fournir directement à PlaceImage
depuis le composant App
.
Vous pouvez déclarer le contexte dans Context.js
.
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Utiliser de grandes images </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }