Choisir la structure de l'état
Bien structurer l’état peut faire toute la différence entre un composant agréable à modifier et déboguer, et un composant qui est une source constante de bugs. Voici des conseils que vous devriez prendre en compte pour structurer vos états.
Vous allez apprendre
- Quand utiliser une vs. plusieurs variables d’état
- Les pièges à éviter en organisant l’état
- Comment résoudre les problèmes courants de structure de l’état
Principes de structuration d’état
Quand vous créez un composant qui contient des états, vous devez faire des choix sur le nombre de variables d’état à utiliser et la forme de leurs données. Même s’il est possible d’écrire des programmes corrects avec une structure d’état sous-optimale, il y a quelques principes qui peuvent vous guider pour faire de meilleurs choix :
- Regroupez les états associés. Si vous mettez tout le temps à jour plusieurs variables d’état à la fois, essayez de les fusionner en une seule variable d’état.
- Évitez les contradictions dans l’état. Quand l’état est structuré de sorte que plusieurs parties d’état puissent être contradictoires, des erreurs peuvent survenir. Essayez d’éviter ça.
- Évitez les états redondants. Si vous pouvez calculer des informations à partir des props du composant ou de ses variables d’état existantes pendant le rendu, vous ne devriez pas mettre ces informations dans un état du composant.
- Évitez la duplication d’états. Quand la même donnée est dupliquée entre plusieurs variables d’état ou dans des objets imbriqués, il est difficile de les garder synchronisées. Réduisez la duplication quand vous le pouvez.
- Évitez les états fortement imbriqués. Un état fortement hiérarchisé n’est pas très pratique à mettre à jour. Quand c’est possible, priorisez une structure d’état plate.
Ces principes visent à rendre l’état simple à actualiser sans créer d’erreurs. Retirer les données redondantes et dupliquées de l’état aide à s’assurer que toutes ses parties restent synchronisées. C’est un peu comme un ingénieur de bases de données qui souhaite « normaliser » la structure de la base de données pour réduire les risques de bugs. Pour paraphraser Albert Einstein : « Faites que votre état soit le plus simple possible — mais pas plus simple. »
Maintenant voyons comment ces principes s’appliquent concrètement.
Regrouper les états associés
Vous hésitez peut-être parfois entre utiliser une ou plusieurs variables d’état.
Devriez-vous faire ça ?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
Ou ça ?
const [position, setPosition] = useState({ x: 0, y: 0 });
Techniquement, les deux approches sont possibles. Mais si deux variables d’état changent toujours ensemble, ce serait une bonne idée de les réunir en une seule variable d’état. Vous n’oublierez ainsi pas ensuite de les garder synchronisées, comme dans cet exemple où les mouvements du curseur mettent à jour les deux coordonnées du point rouge.
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) }
Une autre situation dans laquelle vous pouvez regrouper des données dans un objet ou une liste, c’est lorsque vous ne savez pas à l’avance de combien d’éléments d’état vous aurez besoin. Par exemple, c’est utile pour un formulaire dans lequel l’utilisateur peut ajouter des champs personnalisés.
Évitez les contradictions dans l’état
Voici un questionnaire de satisfaction d’hôtel avec les variables d’état isSending
et isSent
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Merci pour votre retour !</h1> } return ( <form onSubmit={handleSubmit}> <p>Comment était votre séjour au Poney Vagabond ?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Envoyer </button> {isSending && <p>Envoi...</p>} </form> ); } // Prétend envoyer un message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Même si ce code marche, il laisse la place à des états « impossibles ». Par exemple, si vous oubliez d’appeler setIsSent
et setIsSending
ensemble, vous pouvez finir dans une situation où les deux variables isSending
et isSent
sont à true
au même moment. Plus votre composant est complexe, plus il est dur de comprendre ce qu’il s’est passé.
Comme isSending
et isSent
ne doivent jamais être à true
au même moment, il est préférable de les remplacer par une variable d’état status
qui peut prendre l’un des trois états valides : 'typing'
(initial), 'sending'
, et 'sent'
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Merci pour votre retour !</h1> } return ( <form onSubmit={handleSubmit}> <p>Comment était votre séjour au Poney Vagabond ?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Envoyer </button> {isSending && <p>Envoi...</p>} </form> ); } // Prétend envoyer un message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Vous pouvez toujours déclarer quelques constantes pour plus de lisibilité :
const isSending = status === 'sending';
const isSent = status === 'sent';
Mais ce ne sont pas des variables d’état, vous n’avez donc pas à vous soucier de leur désynchronisation.
Évitez les états redondants
Si vous pouvez calculer certaines informations depuis les props d’un composant ou une de ses variables d’état existantes pendant le rendu, vous ne devez pas mettre ces informations dans l’état du composant
Par exemple, prenez ce formulaire. Il marche, mais pouvez-vous y trouver un état redondant ?
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Enregistrons votre arrivée</h2> <label> Prénom :{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Nom :{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Votre ticket sera délivré à : <b>{fullName}</b> </p> </> ); }
Ce formulaire possède trois variables d’état : firstName
, lastName
et fullName
. Cependant, fullName
est redondant. Vous pouvez toujours calculer fullName
depuis firstName
et lastName
pendant le rendu, donc retirez-le de l’état.
Voici comment vous pouvez faire :
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Enregistrons votre arrivée</h2> <label> Prénom :{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Nom :{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Votre ticket sera délivré à : <b>{fullName}</b> </p> </> ); }
Ici, fullName
n’est pas une variable d’état. Elle est plutôt évaluée pendant le rendu :
const fullName = firstName + ' ' + lastName;
Par conséquent, les gestionnaires de changement n’auront rien à faire pour le mettre à jour. Lorsque vous appelez setFirstName
ou setLastName
, vous déclenchez un nouveau rendu, et le prochain fullName
sera calculé à partir des nouvelles données.
En détail
Un exemple commun d’état redondant recourt à ce genre de code :
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
}
Ici, la prop messageColor
est passée comme valeur initiale de la variable d’état color
. Le problème est que si le composant parent transmet une valeur différente dans messageColor
plus tard (par exemple, 'red'
au lieu de 'blue'
), la variable d’état color
ne sera pas mise à jour ! L’état est seulement initialisé lors du rendu initial.
C’est pourquoi la “duplication” de certaines props dans des variables d’état peut être déroutante. Utilisez de préférence directement la prop messageColor
dans votre code. Si vous voulez lui donner un nom plus court, utilisez une constante :
function Message({ messageColor }) {
const color = messageColor;
}
De cette manière, le composant ne sera pas désynchronisé avec la prop transmise par le composant parent.
« Dupliquer » les props dans l’état n’est pertinent que lorsque vous voulez ignorer toutes les mises à jour d’une certaine prop. Par convention, ajoutez initial
ou default
au début du nom de la prop pour préciser que ses nouvelles valeurs seront ignorées :
function Message({ initialColor }) {
// La variable d’état `color` contient la *première* valeur de `initialColor`.
// Les prochains changements à la prop `initialColor` seront ignorés.
const [color, setColor] = useState(initialColor);
}
Évitez la duplication d’états
Ce composant de carte de menu vous permet de choisir un seul en-cas de voyage parmi plusieurs :
import { useState } from 'react'; const initialItems = [ { title: 'bretzels', id: 0 }, { title: 'algues croustillantes', id: 1 }, { title: 'paquet de princes', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>Quel est votre goûter de voyage ?</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>Choisir</button> </li> ))} </ul> <p>Vous avez choisi {selectedItem.title}.</p> </> ); }
À ce stade, il stocke l’élément selectionné en tant qu’objet dans la variable d’état selectedItem
. Cependant, ce n’est pas optimal : le contenu de selectedItem
est le même objet que l’un des éléments de la liste items
. Ça signifie que les informations relatives à l’élément sont dupliquées à deux endroits.
Pourquoi est-ce un problème ? Rendons chaque objet modifiable :
import { useState } from 'react'; const initialItems = [ { title: 'bretzels', id: 0 }, { title: 'algues croustillantes', id: 1 }, { title: 'paquet de princes', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>Quel est votre goûter de voyage ?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>Choisir</button> </li> ))} </ul> <p>Vous avez choisi {selectedItem.title}.</p> </> ); }
Remarquez que si vous cliquez d’abord sur « Choisissez » un élément puis que vous le modifiez, le champ se met à jour, mais le libellé en bas reste inchangé. C’est parce que vous avez dupliqué l’état, et que vous avez oublié de mettre à jour selectedItem
.
Même si vous pourriez également mettre à jour selectedItem
, une solution plus simple consiste à supprimer la duplication. Dans cet exemple, au lieu d’un objet selectedItem
(ce qui crée une duplication des éléments dans items
), vous gardez le selectedId
dans l’état, puis obtenez le selectedItem
en cherchant dans la liste items
un élément avec cet ID :
import { useState } from 'react'; const initialItems = [ { title: 'bretzels', id: 0 }, { title: 'algues croustillantes', id: 1 }, { title: 'paquet de princes', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>Quel est votre goûter de voyage ?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>Choisir</button> </li> ))} </ul> <p>Vous avez choisi {selectedItem.title}.</p> </> ); }
(Vous pouvez également garder l’index sélectionné dans l’état.)
L’état était dupliqué de cette façon :
items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = {id: 0, title: 'pretzels'}
Mais après nos changements, il a la structure suivante :
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
La duplication a disparu, et vous ne conservez que l’état essentiel !
Maintenant si vous modifiez l’élément sélectionné, le message en dessous sera mis à jour immédiatement. C’est parce que setItems
déclenche un nouveau rendu, et items.find(...)
trouve l’élément dont le titre a été mis à jour. Il n’est pas nécessaire de conserver l’objet sélectionné dans l’état, car seul l’ID sélectionné est essentiel. Le reste peut être calculé lors du rendu.
Évitez les états fortement imbriqués
Imaginez un plan de voyage composé de planètes, de continents et de pays. Vous pourriez être tenté·e de structurer son état à l’aide de listes et d’objets imbriqués, comme dans cet exemple :
export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'Terre', childPlaces: [{ id: 2, title: 'Afrique', childPlaces: [{ id: 3, title: 'Botswana', childPlaces: [] }, { id: 4, title: 'Egypte', childPlaces: [] }, { id: 5, title: 'Kenya', childPlaces: [] }, { id: 6, title: 'Madagascar', childPlaces: [] }, { id: 7, title: 'Maroc', childPlaces: [] }, { id: 8, title: 'Nigéria', childPlaces: [] }, { id: 9, title: 'Afrique du Sud', childPlaces: [] }] }, { id: 10, title: 'Amérique', childPlaces: [{ id: 11, title: 'Argentine', childPlaces: [] }, { id: 12, title: 'Brésil', childPlaces: [] }, { id: 13, title: 'Barbade', childPlaces: [] }, { id: 14, title: 'Canada', childPlaces: [] }, { id: 15, title: 'Jamaïque', childPlaces: [] }, { id: 16, title: 'Mexique', childPlaces: [] }, { id: 17, title: 'Trinidad et Tobago', childPlaces: [] }, { id: 18, title: 'Venezuela', childPlaces: [] }] }, { id: 19, title: 'Asie', childPlaces: [{ id: 20, title: 'Chine', childPlaces: [] }, { id: 21, title: 'Hong Kong', childPlaces: [] }, { id: 22, title: 'Inde', childPlaces: [] }, { id: 23, title: 'Singapour', childPlaces: [] }, { id: 24, title: 'Corée du Sud', childPlaces: [] }, { id: 25, title: 'Thaïlande', childPlaces: [] }, { id: 26, title: 'Vietnam', childPlaces: [] }] }, { id: 27, title: 'Europe', childPlaces: [{ id: 28, title: 'Croatie', childPlaces: [], }, { id: 29, title: 'France', childPlaces: [], }, { id: 30, title: 'Allemagne', childPlaces: [], }, { id: 31, title: 'Italie', childPlaces: [], }, { id: 32, title: 'Portugal', childPlaces: [], }, { id: 33, title: 'Espagne', childPlaces: [], }, { id: 34, title: 'Turquie', childPlaces: [], }] }, { id: 35, title: 'Océanie', childPlaces: [{ id: 36, title: 'Australie', childPlaces: [], }, { id: 37, title: 'Bora Bora (Polynésie Française)', childPlaces: [], }, { id: 38, title: 'ïle de Pâques (Chili)', childPlaces: [], }, { id: 39, title: 'Fidji', childPlaces: [], }, { id: 40, title: 'Hawaï (USA)', childPlaces: [], }, { id: 41, title: 'Nouvelle Zélande', childPlaces: [], }, { id: 42, title: 'Vanuatu', childPlaces: [], }] }] }, { id: 43, title: 'Lune', childPlaces: [{ id: 44, title: 'Rheita', childPlaces: [] }, { id: 45, title: 'Piccolomini', childPlaces: [] }, { id: 46, title: 'Tycho', childPlaces: [] }] }, { id: 47, title: 'Mars', childPlaces: [{ id: 48, title: 'Corn Town', childPlaces: [] }, { id: 49, title: 'Green Hill', childPlaces: [] }] }] };
Imaginons maintenant que vous souhaitiez ajouter un bouton pour supprimer un lieu que vous avez déjà visité. Comment procéder ? La mise à jour d’un état imbriqué implique de faire des copies des objets en remontant depuis la partie qui a changé. Supprimer un lieu profondément imbriqué consisterait à copier tous les niveaux supérieurs. Un tel code peut être très long.
Si l’état est trop imbriqué pour être mis à jour facilement, envisagez de « l’aplatir ». Voici une façon de restructurer ces données. Au lieu d’une structure arborescente où chaque lieu possède une liste de ses lieux enfants, chaque lieu peut posséder une liste des ID de ses lieux enfants. Vous pouvez alors stocker une table de correspondance entre chaque ID de lieu et le lieu correspondant.
Cette restructuration des données pourrait vous rappeler une table de base de données :
export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 43, 47], }, 1: { id: 1, title: 'Terre', childIds: [2, 10, 19, 27, 35] }, 2: { id: 2, title: 'Afrique', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypte', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Maroc', childIds: [] }, 8: { id: 8, title: 'Nigéria', childIds: [] }, 9: { id: 9, title: 'Afrique du Sud', childIds: [] }, 10: { id: 10, title: 'Amerique', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentine', childIds: [] }, 12: { id: 12, title: 'Brésil', childIds: [] }, 13: { id: 13, title: 'Barbade', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaïque', childIds: [] }, 16: { id: 16, title: 'Mexique', childIds: [] }, 17: { id: 17, title: 'Trinidad et Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Asie', childIds: [20, 21, 22, 23, 24, 25, 26], }, 20: { id: 20, title: 'Chine', childIds: [] }, 21: { id: 21, title: 'Hong Kong', childIds: [] }, 22: { id: 22, title: 'Inde', childIds: [] }, 23: { id: 23, title: 'Singapour', childIds: [] }, 24: { id: 24, title: 'Corée du Sud', childIds: [] }, 25: { id: 25, title: 'Thaïlande', childIds: [] }, 26: { id: 26, title: 'Vietnam', childIds: [] }, 27: { id: 27, title: 'Europe', childIds: [28, 29, 30, 31, 32, 33, 34], }, 28: { id: 28, title: 'Croatie', childIds: [] }, 29: { id: 29, title: 'France', childIds: [] }, 30: { id: 30, title: 'Allemagne', childIds: [] }, 31: { id: 31, title: 'Italie', childIds: [] }, 32: { id: 32, title: 'Portugal', childIds: [] }, 33: { id: 33, title: 'Espagne', childIds: [] }, 34: { id: 34, title: 'Turquie', childIds: [] }, 35: { id: 35, title: 'Océanie', childIds: [36, 37, 38, 39, 40, 41, 42], }, 36: { id: 36, title: 'Australie', childIds: [] }, 37: { id: 37, title: 'Bora Bora (Polynésie Française)', childIds: [] }, 38: { id: 38, title: 'Ile de Pâques (Chili)', childIds: [] }, 39: { id: 39, title: 'Fidji', childIds: [] }, 40: { id: 40, title: 'Hawaï (USA)', childIds: [] }, 41: { id: 41, title: 'Nouvelle Zélande', childIds: [] }, 42: { id: 42, title: 'Vanuatu', childIds: [] }, 43: { id: 43, title: 'Lune', childIds: [44, 45, 46] }, 44: { id: 44, title: 'Rheita', childIds: [] }, 45: { id: 45, title: 'Piccolomini', childIds: [] }, 46: { id: 46, title: 'Tycho', childIds: [] }, 47: { id: 47, title: 'Mars', childIds: [48, 49] }, 48: { id: 48, title: 'Corn Town', childIds: [] }, 49: { id: 49, title: 'Green Hill', childIds: [] } };
Maintenant que l’état est « plat » (on pourrait aussi dire « normalisé »), mettre à jour des éléments imbriqués devient plus simple.
Désormais, afin d’enlever un lieu, vous n’avez besoin de mettre à jour que deux niveaux d’état :
- La version à jour de son lieu parent devrait exclure l’ID supprimé de sa liste
childIds
. - La version à jour de la « table » racine d’objets doit inclure la version à jour du lieu parent.
Voici un exemple de comment vous pourriez procéder :
import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // Créez une nouvelle version de son lieu parent // cela n’inclut pas l’ID de son enfant. const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // Actulisez l’état de l’objet d’origine... setPlan({ ...plan, // ...pour qu’il ait le parent actualisé [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Lieux à visiter</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Compléter </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
Vous pouvez imbriquer des états autant que vous le souhaitez, mais les rendre « plats » peut résoudre de nombreux problèmes. Ça facilite la mise à jour de l’état, et ça permet de s’assurer qu’il n’y a pas de duplication dans les différentes parties d’un objet imbriqué.
En détail
Idéalement, vous devriez également enlever les éléments supprimés (et leurs enfants !) depuis l’objet « table » pour consommer moins de mémoire. C’est ce que fait cette version. Elle utilise également Immer pour rendre la logique de mise à jour plus concise.
import { useImmer } from 'use-immer'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, updatePlan] = useImmer(initialTravelPlan); function handleComplete(parentId, childId) { updatePlan(draft => { // Enlevez des parents l’ID des endroits enfants const parent = draft[parentId]; parent.childIds = parent.childIds .filter(id => id !== childId); // Oubliez cet endroit et toute sa descendence. deleteAllChildren(childId); function deleteAllChildren(id) { const place = draft[id]; place.childIds.forEach(deleteAllChildren); delete draft[id]; } }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Lieux à visiter</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Compléter </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
Parfois, vous pouvez aussi réduire l’imbrication des états en déplaçant une partie de l’état imbriqué dans les composants enfants. C’est bien adapté aux états éphémères de l’UI qui n’ont pas besoin d’être stockés, comme le fait de savoir si un élément est survolé.
En résumé
- Si deux variables d’état sont toujours mises à jour ensemble, envisagez de les fusionner en une seule.
- Choisissez soigneusement vos variables d’état pour éviter de créer des états « impossibles ».
- Structurez votre état de manière à réduire les risques d’erreur lors de sa mise à jour.
- Evitez les états dupliqués et redondants afin de ne pas avoir à les synchroniser.
- Ne mettez pas de props dans un état à moins que vous ne vouliez spécifiquement empêcher les mises à jour.
- Pour les interactions telles que la sélection d’élément, conservez l’ID ou l’index dans l’état au lieu de l’objet lui-même.
- Si la mise à jour d’un état profondément imbriqué est compliquée, essayez de l’aplatir.
Défi 1 sur 4 · Réparer un composant qui ne s’actualise pas
Ce composant Clock
reçoit deux props : color
et time
. Lorsque vous sélectionnez une couleur différente dans la boîte de sélection, le composant Clock
reçoit une prop color
différente depuis son composant parent. Cependant, la couleur affichée n’est pas mise à jour. Pourquoi ? Corrigez le problème.
import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }