Choisir la structure de l'état
Bien structurer l’état peut créer une différence entre un composant agréable à modifier et débugger, et un qui est une source constante de bugs. Voici des conseils que vous devriez prendre en compte pour structurer l’état.
Vous allez apprendre
- Quand utiliser état unique ou multiple
- Les choses à éviter en organisant l’état
- Résoudre des problèmes connus avec la structure de l’état
Les principes pour structurer l’état
Quand vous créez un comoposant qui contient des états, vous devrez faire des choix sur le nombre de variables d’état à utiliser et quelle sera la forme de leurs données. Même si il est possible d’écrire des programmes corrects avec une structure d’état peu optimale, il y a quelques principes qui peuvent vous guider pour faire des meilleurs choix :
- Les états de groupe. Si vous actualisez tout le temps deux variables d’état ou plus à la fois, essayez de les fusionner en une seule variable d’état.
- Evitez les contradictions dans l’état. Quand l’état est stucturé d’une manière où plusieurs parties d’état peuvent être contradictoires et en “désaccord” entre elles, des erreurs peuvent s’induire. Essayez d’éviter cela.
- Evitez les états redondants. Si vous pouvez calculer des informations à partir des props du composant ou d’une de ses variables d’état pendant l’affichage, vous ne devriez pas mettre ces informations dans un état du composant.
- Evitez la duplication d’états. Quand la même donnée est dupliquée entre plusieurs variables d’état ou dans des objets imbriqués, c’est difficile de les garder synchronisées. Réduisez la duplication quand vous le pouvez.
- Evitez les états fortement imbriqués Un état fortement hiérarchisé n’est pas très pratique à actualiser. Quand c’est possible, priorisez une stucture d’état plate.
Le but derrière ces principes est de rendre l’état simple à actualiser sans ajouter d’erreurs. Retirer des données redondantes et dupliquées de l’état aide à s’assurer que toutes ses parties restent synchronisées. C’est similaire à comment un ingénieur de bases de données souhaite “normaliser” la structure de la base de données pour réduire les risques de bugs. Pour reprendre Albert Einstein, “Créez votre état le plus simple qu’il puisse être, mais pas plus.”
Maintenant voyons comment ces principes s’appliquent concrètement.
Les états de groupe
Vous hésitez peut-être quelques fois entre utiliser une variable d’état simple ou multiple.
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, vous pouvez ces deux approches. Mais si deux variables changent d’état toujours ensemble, une bonne idée serait de les unir en une seule variable d’état. Vous n’oublirez pas par la suite de toujours les garder synchronisées, comme dans cet exemple où les mouvements du curseur actualisent les 2 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> ) }
Un autre cas où vous pouvez regrouper des données dans un objet ou une liste est lorsque vous ne savez pas combien de parties d’état vous aurez besoin. Par exemple, c’est utile quand vous avez un questionnaire dans lequel l’utilisateur peut ajouter des champs personnalisés.
Evitez 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 Prancing Pony ?</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 mieux de les remplacer avec une variable d’état de statut
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 Prancing Pony ?</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 des constantes pour plus de lisibilité :
const isSending = status === 'sending';
const isSent = status === 'sent';
Mais ce ne sont pas des variables d’état, donc vous ne devez pas vous inquiéter sur leur potentielle désynchronisation.
Evitez les états redondants.
Si vous pouvez calculer des informations depuis les props d’un composant ou une de ses variables d’état existantes pendant l’affichage vous ne devriez pas mettre cette information dans l’état du composant
Par exemple, prenez ce questionnaire. Il marche, mais pouvez-vous trouver des états redondants dans celui-ci ?
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 questionnaire possède trois variables d’état : firstName
, lastName
et fullName
. Cependant, fullName
est redondant. Vous pouvez toujours calculer fullName
depuis firstName
et lastName
pendant l’affichage, donc retirez-le de l’état.
Voilà 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. À la place, elle est évaluée pendant l’affichage :
const fullName = firstName + ' ' + lastName;
Par conséquent, les gestionnaires de changement n’auront rien à faire pour l’actualiser. Quand vous appelez setFirstName
ou setLastName
, vous déclenchez une actualisation de l’affichage, et le prochain fullName
sera calculé avec les nouvelles données.
En détail
Un exemple commun d’état redondant est un code de ce type :
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
}
Ici, la variable d’état color
est initialisée à la valeur de la prop messageColor
. Le problème est que si le composant parent passe une valeur différente dans messageColor
plus tard (par exemple, 'rouge'
au lieu de 'bleu'
), la variable d’état color
ne serait pas mise à jour ! L’état est initialisé seulement durant le premier rendu.
C’est pourquoi la duplication de certaines props dans des variables d’état peut amener à la confusion. À la place, utilisez la prop messageColor
directement 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 passée par le composant parent.
Dupliquer les props dans l’état fait sens uniquement quand vous voulez ignorer toutes les mises à jour d’une prop spécifique. Par convention, commencez le nom de la prop par initial
ou default
pour préciser que ses nouvelles valeurs seront ignorées :
function Message({ initialColor }) {
// La variable d’état `couleur` contient la *première* valeur de `initialColor`.
// Les prochains changements à la prop `initialColor` sont ignorées.
const [color, setColor] = useState(initialColor);
}
Evitez la duplication d’états
Ce composant-liste vous laisse choisir un seul voyage parmis 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> </> ); }
Actuellement, il stocke l’objet selectionné 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 objets dans la liste items
. Cela signifie que les informations sur l’objet lui-même sont dupliquées en 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> </> ); }
Notez que si vous cliquez sur “Choisir” en premier sur un objet puis l’éditez, la saisie se met à jour, mais le label en bas est différent des modifications. C’est parce que vous avez un état dupliqué, et vous avez oublié de mettre à jour selectedItem
.
Même si vous pourriez mettre à jour selectedItem
également, une correction plus simple serait de retirer la duplication. Dans cet exemple, au lieu d’un objet selectedItem
(qui crée une duplication des objets dans items
), vous gardez le selectedId
dans l’état, et ensuite obtenez le selectedItem
en cherchant dans la liste items
pour un objet 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> </> ); }
(Autrement, vous pouvez 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 les changements, il est ainsi :
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
La duplication est finie, et vous gardez seulement l’état essentiel !
Maintenant si vous modifiez l’objet selectionné, le message en dessous sera mis à jour immédiatement. C’est parce que setItems
déclenche un rendu, et items.find(...)
doit trouver l’objet avec le titre mis à jour. Vous n’aviez pas besoin de garder l’objet selectionné dans l’état, parce que seulement l’ID sélectionné est essentiel. Le reste pourrait être calculé pendant le rendu.
Evitez les états fortement imbriqués
Imaginez un plan de voyage composé de planètes, de continents et de pays. Vous serez sûrement tentés de structurer son état en utlisant des listes et des 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: [] }] }] };
Maintenant disons que vous voulez ajouter un bouton pour supprimer un endroit que vous avez déjà visité. Comment feriez-vous ? Actualiser des états imbriqués inclus de faire des copies d’objets depuis l’endroit qui a changé. Supprimer un endroit imbriqué profondément consisterait à copier tout son chemin d’emplacement. Un tel code peut être très long.
Si l’état est trop imbriqué pour s’actualiser facilement, préférez le faire “plat”. C’est une manière pour restructurer cette donnée. Au lieu d’une structure arborescente où chaque place
a une liste des emplacements de ses enfants, chaque endroit peut posséder une liste de l’ID des emplacements de ses enfants. Stockez une corrélation depuis chaque ID de place
à la place
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” (aussi dit “normalisé”), actualiser les éléments imbriqués devient plus simple
Afin d’enlever un endroit désormais vous avez seulement besoin d’actualiser deux niveaux d’état:
- La version actualisée de son endroit parent devrait exclure l’ID enlevé de sa liste
childIds
. - La version actualisée du “tableau” d’objet root devrait inclure la version actualisée de l’endroit parent.
Voici un exemple de commment vous devriez commencer:
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 endroit 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>Endroits à 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 voulez, mais les rendre “plats” peut résoudre beaucoup de problèmes. Cela rend les états plus simple à actualiser, et cela aide à être sûr que vous n’avez pas de duplication dans les différentes parties d’un objet imbriqué.
En détail
Idéalement, vous voudrez aussi enlever les éléments supprimés (et leurs enfants !) depuis l’objet “tableau” pour améliorer l’utilisation de la mémoire. Cette version fait cela. Cela utilise également Immer pour faire de la logique d’actualisation plus courte.
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>Endroits à 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’état d’imbrication en déplaçant des états imbriqués dans les composants enfants. Cela fonctionne bien pour les états UI éphémères qui n’ont pas besoin d’être stockés, comme quand un objet est survolé.
En résumé
- Si deux variables d’état s’actualisent toujours ensemble, pensez à les fusionner en une seule.
- Choississez votre variable d’état précautionneusement pour éviter de créer des “états” impossibles.
- Structurez votre état de manière à réduire les chances que vous fassiez une erreur en l’actualisant.
- Evitez les états dupliqués et redondants pour que vous n’ayez pas besoin de le garder synchronnisés.
- Ne mettez pas des props dans un état sauf si vous voulez spécifiquement prévenir les mises à jours.
- Pour les évènements d’UI tels que la sélection, gardez l’ID ou index dans l’état plutôt que l’objet lui-même.
- Si actualiser profondément l’état imbriqué est compliqué, 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
. Quand vous sélectionnez une couleur différente dans la boîte de sélection, le composant Clock
reçoit une prop color
différente de son composant parent. Cependant, la couleur affichée ne s’actualise pas. Pourquoi ? Réglez 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> ); }