Préserver et réinitialiser l'état
L’état est isolé entre les composants. React garde en mémoire quel état appartient à quel composant en fonction de leur place dans l’arbre de l’interface utilisateur (l’UI). Vous pouvez contrôler quand préserver l’état et quand le réinitialiser entre les différents rendus.
Vous allez apprendre
- Comment React « voit » les structures des composants
- Quand React choisit de préserver ou de réinitialiser l’état
- Comment forcer React à réinitialiser l’état d’un composant
- Comment les clés et les types déterminent si l’état est préservé ou non
L’arbre de l’UI
Les navigateurs utilisent différentes structures arborescentes pour modéliser l’UI. Le DOM représente les éléments HTML, le CSSOM fait la même chose pour le CSS. Il existe même un arbre d’accessibilité !
React utilise également des structures arborescentes pour gérer et modéliser l’UI que vous réalisez. React crée des arbres d’UI à partir de votre JSX. Ensuite, React DOM met à jour les éléments DOM du navigateur pour qu’ils correspondent à cet arbre de l’UI (React Native retranscrit ces arbres en composants spécifiques aux plateformes mobiles).
L’état est lié à une position dans l’arbre
Lorsque vous donnez un état à un composant, vous pouvez penser que l’état « vit » à l’intérieur du composant. En réalité, l’état est conservé à l’intérieur de React. React associe chaque élément d’état qu’il conserve au composant correspondant en fonction de la place que celui-ci occupe dans l’arbre de l’UI.
Ci-dessous, il n’y a qu’une seule balise <Counter />
, pourtant elle est affichée à deux positions différentes :
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Voici comment les visualiser sous forme d’arbre :
Il s’agit de deux compteurs distincts car chacun d’eux a sa propre position dans l’arbre. Généralement, vous n’avez pas besoin de penser à ces positions pour utiliser React, mais il peut être utile de comprendre comment ça fonctionne.
Dans React, chaque composant à l’écran a son propre état complétement isolé. Par exemple, si vous affichez deux composants Counter
l’un à côté de l’autre, chacun d’eux aura ses propres variables d’état indépendantes de score
et d’hover
.
Cliquez sur chaque compteur et constatez qu’ils ne s’affectent pas l’un l’autre :
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Comme vous pouvez le voir, quand un compteur est mis à jour, seul l’état de ce composant est mis à jour :
React conservera l’état tant que vous afficherez le même composant à la même position. Pour vous en rendre compte, incrémentez les deux compteurs, puis supprimez le deuxième composant en décochant « Afficher le deuxième compteur », et enfin remettez-le en cochant à nouveau la case :
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Afficher le deuxième compteur </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Remarquez qu’au moment où vous cessez d’afficher le deuxième compteur, son état disparaît complètement. Lorsque React supprime un composant, il supprime également son état.
Lorsque vous cochez « Afficher le deuxième compteur », un deuxième Counter
avec son état associé sont initialisés de zéro (score = 0
), puis ajoutés au DOM.
React préserve l’état d’un composant tant qu’il est affiché à sa position dans l’arbre de l’UI. S’il est supprimé, ou si un composant différent est affiché à la même position, alors React se débarrasse de son état.
Le même composant à la même position préserve son état
Dans cet exemple, il y a deux balises <Counter />
différentes :
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Utiliser un style fantaisiste </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Quand vous cochez ou décochez la case, l’état du compteur n’est pas réinitialisé. Que isFancy
soit à true
ou à false
, vous avez toujours un <Counter />
comme premier enfant du div
renvoyé par le composant racine App
:
C’est le même composant à la même position, donc du point de vue de React, il s’agit du même compteur.
Des composants différents à la même position réinitialisent l’état
Dans cet exemple, cliquer sur la case remplacera <Counter>
par un <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>À bientôt !</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Faire une pause </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Vous basculez ici entre deux types de composants différents à la même position. À l’origine, le premier enfant du <div>
contenait un Counter
. Ensuite, comme vous l’avez échangé avec un p
, React a supprimé le Counter
de l’UI et détruit son état.
Ainsi, quand vous faites le rendu d’un composant différent à la même position, l’état de tout son sous-arbre est réinitialisé. Pour comprendre comment ça fonctionne, incrémentez le compteur et cochez la case :
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Utiliser un style fantaisiste </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
L’état du compteur se réinitialise quand vous cliquez sur la case. Bien que vous affichiez un Counter
, le premier enfant du div
passe d’un div
à une section
. Lorsque l’enfant div
a été retiré du DOM, tout l’arbre en dessous de lui (ce qui inclut le Counter
et son état) a également été détruit.
De manière générale, si vous voulez préserver l’état entre les rendus, la structure de votre arbre doit « correspondre » d’un rendu à l’autre. Si la structure est différente, l’état sera détruit car React détruit l’état quand il enlève un composant de l’arbre.
Réinitialiser l’état à la même position
Par défaut, React préserve l’état d’un composant tant que celui-ci conserve sa position. Généralement, c’est exactement ce que vous voulez, c’est donc logique qu’il s’agisse du comportement par défaut. Cependant, il peut arriver que vous vouliez réinitialiser l’état d’un composant. Regardez cette appli qui permet à deux joueurs de surveiller leur score pendant leur tour :
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Clara" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Joueur suivant ! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Score de {person} : {score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Pour le moment, le score est conservé quand vous changez de joueur. Les deux Counter
apparaissent à la même position, donc React les voit comme le même Counter
dont la prop person
a changé.
Conceptuellement, dans cette appli, ils doivent être considérés comme deux compteurs distincts. Ils apparaissent certes à la même place dans l’UI, mais l’un est pour Clara, l’autre pour Sarah.
Il y a deux façons de réinitialiser l’état lorsqu’on passe de l’un à l’autre :
- Afficher les composants à deux positions différentes.
- Donner explicitement à chaque composant une identité avec
key
.
Option 1 : changer la position du composant
Si vous souhaitez rendre ces deux Counter
indépendants, vous pouvez les afficher à deux positions différentes :
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Clara" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Joueur suivant ! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Score de {person} : {score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
- Initialement,
isPlayerA
vauttrue
. Ainsi la première position contient l’état deCounter
et la seconde est vide. - Quand vous cliquez sur le bouton « Joueur suivant », la première position se vide et la seconde contient désormais un
Counter
.
Chaque état de Counter
est supprimé dès que ce dernier est retiré du DOM. C’est pour ça qu’il est réinitialisé à chaque fois que vous appuyez sur le bouton.
Cette solution est pratique quand vous n’avez qu’un petit nombre de composants indépendants à afficher à la même position dans l’arbre. Dans cet exemple, vous n’en avez que deux, ce n’est donc pas compliqué de faire leurs rendus séparément dans le JSX.
Option 2 : réinitialiser l’état avec une clé
Il existe une méthode plus générique pour réinitialiser l’état d’un composant.
Vous avez peut-être déjà vu les key
lors de l’affichage des listes. Ces clés ne sont pas réservées aux listes ! Vous pouvez les utiliser pour aider React à faire la distinction entre n’importe quels composants. Par défaut, React utilise l’ordre dans un parent (« premier compteur », « deuxième compteur ») pour différencier les composants. Les clés vous permettent de dire à React qu’il ne s’agit pas simplement d’un premier compteur ou d’un deuxième compteur, mais plutôt un compteur spécifique — par exemple le compteur de Clara. De cette façon, React reconnaîtra le compteur de Clara où qu’il apparaisse dans l’arbre.
Dans cet exemple, les deux <Counter />
ne partagent pas leur état, bien qu’ils apparaissent à la même position dans le JSX :
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Clara" person="Clara" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Joueur suivant ! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>Score de {person} : {score}</h1> <button onClick={() => setScore(score + 1)}> Incrémenter </button> </div> ); }
Remplacer Clara par Sarah ne préserve pas l’état. C’est parce que vous leur avez donné des key
différentes :
{isPlayerA ? (
<Counter key="Clara" person="Clara" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Le fait de spécifier une key
indique à React de l’utiliser également comme élément de position, plutôt que son ordre au sein du parent. Ainsi, même si vous faites le rendu à la même position dans le JSX, React les voit comme deux compteurs distincts qui ne partageront jamais leur état. À chaque fois qu’un compteur apparaît à l’écran, son état est créé. À chaque fois qu’il est supprimé, son état est supprimé. Passer de l’un à l’autre réinitialise leur état, encore et encore.
Réinitialiser un formulaire avec une clé
Réinitialiser un état avec une clé s’avère particulièrement utile quand on manipule des formulaires.
Dans cette appli de discussions, le composant <Chat>
contient l’état du champ de saisie :
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Clara', email: 'clara@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Essayez de saisir quelque chose dans le champ, puis appuyez sur « Alice » ou « Bob » pour choisir un destinataire différent. Vous noterez que le champ de saisie est conservé parce que le <Chat>
est affiché à la même position dans l’arbre.
Dans beaucoup d’applis, c’est le comportement désiré, mais pas dans cette appli de discussion ! Vous ne souhaitez pas qu’un utilisateur envoie un message qu’il a déjà tapé à la mauvaise personne à la suite d’un clic malencontreux. Pour corriger ça, ajoutez une key
:
<Chat key={to.id} contact={to} />
Ça garantit que lorsque vous sélectionnez un destinataire différent, le composant Chat
sera recréé de zéro, ce qui inclut tout l’état dans l’arbre en dessous. React recréera également tous les éléments DOM plutôt que de les réutiliser.
Désormais, changer de destinataire vide le champ de saisie :
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Clara', email: 'clara@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
En détail
Dans une véritable appli de discussion, vous souhaiterez probablement récupérer l’état de la saisie lorsque l’utilisateur resélectionne le destinataire précédent. Il existe plusieurs manières de garder « vivant » l’état d’un composant qui n’est plus visible :
- Vous pouvez afficher tous les chats plutôt que le seul chat actif, mais en masquant les autres avec du CSS. Les chats ne seraient pas supprimés de l’arbre, de sorte que leur état local serait préservé. Cette solution fonctionne très bien pour des UI simples. Cependant, ça peut devenir très lent si les arbres cachés sont grands et contiennent de nombreux nœuds DOM.
- Vous pouvez faire remonter l’état et conserver dans le composant parent le message en attente pour chaque destinataire. De cette façon, le fait que les composants enfants soient supprimés importe peu car c’est le parent qui conserve les informations importantes. C’est la solution la plus courante.
- Vous pouvez aussi utiliser une source différente en plus de l’état React. Par exemple, vous souhaitez sans doute qu’un brouillon du message persiste même si l’utilisateur ferme accidentellement la page. Pour implémenter ça, vous pouvez faire en sorte que le composant
Chat
intialise son état en lisant lelocalStorage
et y sauve également les brouillons.
Quelle que soit votre stratégie, une discussion avec Alice est conceptuellement différente d’une autre avec Bob, il est donc naturel de donner une key
à l’arbre <Chat>
en fonction du destinataire actuel.
En résumé
- React conserve l’état tant que le même composant est affiché à la même position.
- L’état n’est pas conservé dans les balises JSX. Il est associé à la position dans l’arbre où vous placez ce JSX.
- Vous pouvez forcer un sous-arbre à réinitialiser son état en lui donnant une clé différente.
- N’imbriquez pas les définitions de composants ou vous allez accidentellement réinitialiser leur état.
Défi 1 sur 5 · Corriger une saisie qui disparaît
Cet exemple affiche un message quand vous appuyez sur le bouton. Cependant, appuyer sur ce bouton vide aussi le champ de saisie par accident. Pourquoi ? Corrigez ça pour que le champ de saisie ne se vide pas quand on appuie sur le bouton.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Indice : votre ville préférée ?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Cacher l'indice</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Afficher l'indice</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }