Partager l’état entre des composants
Vous souhaitez parfois que les états de deux composants changent toujours ensemble. Pour ça, retirez leurs états et fusionnez-les dans leur plus proche parent commun, puis passez-leur ces données via les props. C’est ce qu’on appelle faire remonter l’état, et c’est l’une des choses les plus communes que vous ferez en écrivant du code React.
Vous allez apprendre
- Comment partager l’état entre composants en le faisant remonter
- Ce que sont les composants contrôlés et non contrôlés
Faire remonter l’état par l’exemple
Dans cet exemple, un composant parent Accordion
affiche deux Panel
distincts :
Accordion
Panel
Panel
Chaque composant Panel
a une variable d’état booléenne isActive
qui définit si le contenu du panneau est visible ou non.
Appuyez sur le bouton Afficher pour les deux panneaux :
Error
Extra 185 of 186 byte(s) found at buffer[1]
Constatez que le fait d’appuyer sur le bouton d’un des panneaux n’affecte pas l’affichage de l’autre panneau : ils sont indépendants.
Supposons maintenant que vous souhaitiez le modifier afin qu’un seul panneau soit déplié à la fois. Avec cette conception, étendre le second panneau devrait replier le premier. Comment feriez-vous ça ?
Afin de coordonner ces deux panneaux, vous devez « faire remonter leur état » jusqu’à un composant parent en trois étapes :
- Retirez les états des composants enfants.
- Passez des valeurs codées en dur depuis le parent commun.
- Ajoutez l’état au parent commun et passez-le aux enfants avec les gestionnaires d’événements.
Ça permettra au composant Accordion
de coordonner les deux Panel
et ainsi n’en déplier qu’un seul à la fois.
Étape 1 : retirez l’état des composants enfants
Vous donnerez le contrôle du isActive
des Panel
au composant parent. Ça signifie que le composant parent passera isActive
aux Panel
via les props. Commencez par supprimer cette ligne du composant Panel
:
const [isActive, setIsActive] = useState(false);
À la place, ajoutez isActive
à la liste des props de Panel
:
function Panel({ title, children, isActive }) {
Désormais, le composant parent du Panel
peut contrôler isActive
en le passant en tant que prop. À l’inverse, le composant Panel
n’a plus le contrôle sur la valeur de isActive
— elle est désormais entre les mains du composant parent !
Étape 2 : passez la valeur codée en dur depuis le parent commun
Pour faire remonter l’état, vous devez identifier le parent commun le plus proche des deux composants enfants que vous souhaitez coordonner :
Accordion
(parent commun le plus proche)Panel
Panel
Dans cet exemple, il s’agit du composant Accordion
. Puisqu’il se situe au-dessus des deux panneaux et qu’il peut contrôler leurs props, il devient la « source de vérité » pour savoir quel panneau est actuellement actif. Faites en sorte que le composant Accordion
passe la valeur codée en dur de isActive
(par exemple true
) aux deux panneaux :
import { useState } from 'react'; export default function Accordion() { return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="À propos" isActive={true}> Avec une population d'environ 2 millions d'habitants, Almaty est la plus grande ville du Kazakhstan. De 1929 à 1997, elle en a été la capitale. </Panel> <Panel title="Étymologie" isActive={true}> Le nom vient de <span lang="kk-KZ">алма</span>, le mot kazakh pour « pomme » ; il est souvent traduit par « pleine de pommes ». En fait, la région entourant Almaty est considérée comme le berceau ancestral de la pomme, et la <i lang="la">Malus sieversii</i> sauvage est considérée comme une probable candidate pour être l'ancêtre de la pomme domestique moderne. </Panel> </> ); } function Panel({ title, children, isActive }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> Afficher </button> )} </section> ); }
Modifiez les valeurs de isActive
codées en dur dans le composant Accordion
et voyez le résultat à l’écran.
Étape 3 : ajoutez l’état au parent commun
Faire remonter l’état change souvent la nature de ce que vous y stockez.
Dans cet exemple, un seul panneau doit être actif à un instant donné. Ça signifie que le parent commun Accordion
doit garder trace de quel panneau est actif. Pour la variable d’état, il pourrait utiliser un nombre représentant l’index du Panel
actif plutôt qu’un boolean
:
const [activeIndex, setActiveIndex] = useState(0);
Quand activeIndex
vaut 0
, le premier panneau est actif, et quand il vaut 1
, alors c’est le second.
Le fait d’appuyer sur le bouton « Afficher » dans l’un ou l’autre des Panel
doit modifier l’index actif dans Accordion
. Un Panel
ne peut définir l’état activeIndex
directement puisqu’il est défini à l’intérieur d’Accordion
. Ce dernier doit explicitement autoriser le composant Panel
à changer son état en lui transmettant un gestionnaire d’événement en tant que prop :
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
Le <button>
à l’intérieur du Panel
va maintenant utiliser la prop onShow
comme gestionnaire d’événement pour le clic :
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="À propos" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > Avec une population d'environ 2 millions d'habitants, Almaty est la plus grande ville du Kazakhstan. De 1929 à 1997, elle en a été la capitale. </Panel> <Panel title="Étymologie" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > Le nom vient de <span lang="kk-KZ">алма</span>, le mot kazakh pour « pomme » ; il est souvent traduit par « pleine de pommes ». En fait, la région entourant Almaty est considérée comme le berceau ancestral de la pomme, et la <i lang="la">Malus sieversii</i> sauvage est considérée comme une probable candidate pour être l'ancêtre de la pomme domestique moderne. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Afficher </button> )} </section> ); }
Ainsi s’achève la partie pour faire remonter l’état ! Déplacer l’état dans le composant parent commun vous permet de coordonner les deux panneaux. L’utilisation d’un index actif à la place de deux indicateurs « est affiché » garantit qu’un seul panneau est actif à la fois. Enfin, passer le gestionnaire d’événement à l’enfant permet à ce dernier de changer l’état du parent.
En détail
Il est courant d’appeler « non contrôlé » un composant ayant un état local. Par exemple, le composant Panel
d’origine avec une variable d’état isActive
est non contrôlé parce que son parent ne peut pas influencer le fait que le panneau soit actif ou non.
À l’inverse, vous pouvez dire qu’un composant est « contrôlé » quand les informations importantes qu’il contient sont pilotées par les props plutôt que par son propre état local. Ça permet au parent de spécifier entièrement son comportement. Le composant final Panel
avec la prop isActive
est contrôlé par le composant Accordion
.
Les composants non contrôlés sont plus simples d’utilisation au sein de leurs parents puisqu’ils nécessitent moins de configuration. Cependant, ils sont moins flexibles lorsque vous souhaitez les coordonner . Les composants contrôlés sont extrêmement flexibles, mais leurs composants parents doivent les configurer complètement avec des props.
Dans la pratique, « contrôlé » et « non contrôlé » ne sont pas des termes techniques stricts — chaque composant dispose généralement d’un mélange entre état local et props. Ça reste une façon pratique de décrire comment les composants sont conçus et les capacités qu’ils offrent.
En écrivant un composant, déterminez quelles informations doivent être contrôlées (via les props) ou non contrôlées (via l’état). De toutes façons, vous pouvez toujours changer d’avis par la suite et remanier le composant.
Une source de vérité unique pour chaque état
Dans une application React, de nombreux composants auront leur propre état. Ces derniers peuvent « vivre » à proximité des composants feuilles (les composants en bas de l’arbre) tels que les champs de saisie. D’autres peuvent « vivre » plus proches de la racine de l’appli. Ainsi, même les bibliothèques de routage côté client sont généralement implémentées en stockant la route actuelle dans un état React, puis en la transmettant au travers des props !
Pour chaque élément unique de l’état, vous devrez choisir le composant auquel il va « appartenir ». Ce principe est également connu comme une « source de vérité unique » (lien en anglais, NdT). Ça ne signifie pas que tout l’état se situe à un seul endroit mais plutôt que pour chaque élément d’état, il y a un composant spécifique qui détient cet élément. Au lieu de dupliquer un état partagé entre des composants, il faut le faire remonter jusqu’à leur parent commun, puis le passer vers le bas aux composants qui en ont besoin.
Votre appli évoluera au fur et à mesure que vous y travaillerez. Il est fréquent que vous déplaciez un état vers le bas ou vers le haut alors que vous êtes en train de déterminer où chaque élément de l’état doit « vivre ». Ça fait partie du processus !
Pour voir ce que ça donne en pratique avec quelques composants supplémentaires, lisez Penser en React.
En résumé
- Lorsque vous souhaitez coordonner deux composants, déplacez leur état vers leur parent commun.
- Ensuite, passez l’information vers le bas via les props provenant de leur parent commun.
- Enfin, transmettez des gestionnaires d’événements afin que les enfants puissent changer l’état du parent.
- Il est utile de considérer les composants comme étant « contrôlés » (pilotés par les props) ou « non contrôlés » (pilotés par l’état).
Défi 1 sur 2 · Saisies synchronisées
Ces deux champs de saisie sont indépendants. Faites en sorte qu’ils soient synchronisés : modifier l’un des champs doit également mettre à jour l’autre champ avec le même texte, et vice-versa.
import { useState } from 'react'; export default function SyncedInputs() { return ( <> <Input label="Premier champ" /> <Input label="Deuxième champ" /> </> ); } function Input({ label }) { const [text, setText] = useState(''); function handleChange(e) { setText(e.target.value); } return ( <label> {label} {' '} <input value={text} onChange={handleChange} /> </label> ); }