- Publié le
Le guide ultime pour maîtriser l’état dans React et dire adieu aux incohérences et aux bugs 🐞 !
- Auteurs
- Nom
- Cédric RIBALTA
Pourquoi réduire les états dans vos composants React peut prévenir de gros bugs 🐛
En tant que développeur, il t'est sûrement arrivé de revoir une Pull Request (PR) ou une Merge Request (MR) et de remarquer un composant React qui contient trop de variables d’état contradictoires. Ce phénomène, malheureusement courant, complique la logique de l'application et augmente la probabilité de bugs. Explorons pourquoi il est crucial de réduire le nombre d’états dans tes composants React et comment y parvenir.
Simplifiez, c'est gagné ! 🎯
La documentation de React met en garde contre l’excès de variables d’état. En effet, chaque morceau d’état représente une "pièce mobile". Plus tu as de "pièces mobiles", plus la logique de ton application devient complexe… la complexité étant la meilleure amie des bugs.
Prenons un exemple simple :
const [isEmpty, setIsEmpty] = useState(true)
const [isTyping, setIsTyping] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
En regardant cet extrait, on comprend vite que l’état du composant pourrait devenir incohérent. Imagine un instant que isTyping
et isSubmitting
soient tous deux true
. Ce paradoxe serait impossible à gérer pour l’interface utilisateur, car le composant ne pourrait pas être en mode "saisie" et en mode "envoi" simultanément.
Comprendre la différence entre UI Impérative et Déclarative 🤔
Dans une application classique, on pourrait penser à chaque interaction utilisateur en termes de manipulations directes : activer, désactiver, afficher, ou masquer des éléments. C'est ce qu'on appelle l'approche impérative. Par exemple, imagine que l'utilisateur interagit avec un formulaire :
- Lorsque le champ de saisie contient du texte, le bouton "Envoyer" s'active.
- En appuyant sur "Envoyer", le formulaire devient désactivé et un spinner apparaît.
- Si la requête réseau réussit, le formulaire disparaît et un message de remerciement s’affiche.
- Si la requête échoue, le message d’erreur apparaît, et le formulaire redevient actif pour permettre une nouvelle tentative.
En programmation impérative, tu dois donner des instructions spécifiques pour chaque étape de cette interaction. Cela devient rapidement difficile à gérer, surtout dans les applications complexes.
React adopte une approche déclarative. Au lieu de manipuler directement chaque composant, tu déclares l'état final souhaité. React se charge ensuite de calculer les transitions nécessaires pour mettre à jour l’interface. C’est comme monter dans un taxi et donner la destination au lieu de dicter chaque virage. En adoptant cette approche, tu rends ton code plus clair et plus facile à maintenir.
Et si en plus tes composants sont purs (sans effets de bord), tu peux être sûr que le composant affichera toujours le bon état, quelles que soient les interactions de l’utilisateur.
Les risques des états contradictoires ⚠️
Lorsqu’on duplique ou multiplie les variables d’état pour un même comportement visuel, on s'expose à plusieurs problèmes :
- État incohérent : Des variables peuvent entrer en conflit.
- Maintenance difficile : Plus le nombre de variables d’état est élevé, plus le composant devient difficile à lire et à maintenir.
- Bugs difficiles à détecter : Des états non synchronisés risquent de déclencher des comportements inattendus.
Gérer les changements d’état en fonction des entrées utilisateur et machine ⚙️
Dans React, les changements d'état peuvent être déclenchés par deux types d'entrées :
- Entrées humaines : par exemple, la saisie de texte ou le clic sur un bouton.
- Entrées machines : comme une réponse de requête réseau ou la fin d’un délai (timeout).
Prenons l'exemple du formulaire. Voici comment chaque action déclenche un changement d'état dans notre composant :
- Saisie de texte (entrée humaine) : lorsque l’utilisateur tape, l’état passe de
isEmpty
àisTyping
. - Clic sur "Envoyer" (entrée humaine) : cela déclenche le passage à l’état
isSubmitting
. - Réponse réseau réussie (entrée machine) : l’état devient
success
, et le message de remerciement s’affiche. - Réponse réseau échouée (entrée machine) : l’état devient
error
, et un message d’erreur apparaît.
Ces étapes permettent de structurer les changements de façon claire et maintenable. Avec cette méthode, tu peux anticiper toutes les "étapes" visuelles et fonctionnelles du composant avant même de créer la logique sous-jacente, en identifiant tous les états possibles : Empty
, Typing
, Submitting
, Success
, et Error
.
Comment gérer efficacement l’état dans tes composants 💡
Pour résoudre ces problèmes, il est essentiel de minimiser et de centraliser l’état. Voici quelques stratégies tirées de la documentation de React pour y parvenir :
Éliminer les états dérivés Si une variable d’état peut être calculée à partir d’une autre, elle n’est probablement pas nécessaire. Par exemple,
isEmpty
pourrait être déterminé paranswer.length === 0
au lieu d’avoir une variable d’état distincte.Regrouper les états connexes Utiliser une variable d’état unique pour représenter plusieurs états visuels interdépendants. Par exemple, au lieu de cinq variables booléennes (
isTyping
,isSubmitting
,isSuccess
, etc.), créer une variablestatus
avec des valeurs comme'typing'
,'submitting'
, ou'success'
:
const [status, setStatus] = useState('typing') // 'typing', 'submitting', 'success'
- Éviter les duplication d’états Les duplications d’état entraînent souvent des incohérences. Si une information est déjà disponible via une autre variable, il faut éviter de créer un nouvel état. Par exemple,
isError
pourrait être remplacé parerror !== null
.
Exemple avant/après refactoring ✨
Prenons l’exemple complet pour illustrer le refactoring de l'état dans un composant :
const [answer, setAnswer] = useState('')
const [isEmpty, setIsEmpty] = useState(true)
const [isTyping, setIsTyping] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [isError, setIsError] = useState(false)
Après le refactoring :
const [answer, setAnswer] = useState('')
const [status, setStatus] = useState('typing') // 'typing', 'submitting', 'success'
const [error, setError] = useState(null)
Ce refactoring réduit le nombre d'états à l'essentiel et rend le code beaucoup plus lisible. Tu peux être certain que ces trois variables sont nécessaires pour le bon fonctionnement du composant, car chacune d'elles est directement liée aux comportements visuels du composant.
Étape suivante : extraire la Logique des états dans un Reducer 🛠️
Une fois que tu as simplifié et centralisé l’état de votre composant, l’étape suivante consiste à extraire cette logique d’état dans un reducer. Utiliser un reducer permet de structurer et d’organiser les changements d’état de manière plus claire, notamment lorsque l’application devient plus complexe et que les transitions d’état se multiplient.
Dans React, useReducer
est une fonction similaire à useState
, mais qui est particulièrement adaptée aux cas où tu gères plusieurs variables d’état interdépendantes. Elle utilise un reducer, qui est une fonction prenant en entrée l’état actuel et une action, et qui retourne le nouvel état.
Imaginons notre formulaire, dont les états possibles sont typing
, submitting
, success
, et error
. Voici un exemple simple de reducer pour gérer ces transitions d’état :
import { useReducer } from 'react'
const initialState = {
answer: '',
status: 'empty', // 'typing', 'submitting', 'success', 'error'
error: null,
}
function formReducer(state, action) {
switch (action.type) {
case 'TYPING':
return { ...state, status: 'typing', answer: action.payload }
case 'SUBMITTING':
return { ...state, status: 'submitting' }
case 'SUCCESS':
return { ...state, status: 'success', answer: '' }
case 'ERROR':
return { ...state, status: 'error', error: action.payload }
default:
return state
}
}
Ensuite, dans ton composant, tu peux utiliser useReducer pour initialiser cet état et gérer les actions :
const [state, dispatch] = useReducer(formReducer, initialState)
// Exemple d’utilisation dans un gestionnaire d’événements
function handleSubmit() {
dispatch({ type: 'SUBMITTING' })
// Simulation d’une requête réseau
fakeApiCall()
.then(() => dispatch({ type: 'SUCCESS' }))
.catch((error) => dispatch({ type: 'ERROR', payload: error.message }))
}
Pourquoi utiliser un Reducer ? 🚀
En utilisant un reducer, tu gagnes en lisibilité et en centralisation. La logique de changement d’état est clairement définie dans une seule fonction (le reducer), ce qui permet de gérer les transitions de manière prévisible. Cela devient particulièrement utile pour éviter les contradictions dans les états, car chaque action est gérée de manière explicite, ce qui simplifie le débogage et améliore la maintenabilité.
Cette approche offre également une meilleure évolutivité. Si de nouveaux états ou transitions sont ajoutés à l’application, tu peux les intégrer directement dans le reducer sans risquer de fragmenter la logique de gestion des états dans plusieurs fonctions ou hooks.
En appliquant cette technique, tu amènes la gestion d’état de ton composant à un niveau de clarté et de robustesse bien supérieur, ce qui est particulièrement précieux pour les applications plus vastes et les projets collaboratifs. 🎉
En conclusion
Réduire le nombre de variables d’état dans tes composants React peut transformer ton code en un composant plus propre, plus lisible, et moins sujet aux bugs. La clé est de rester simple et de ne garder que ce qui est essentiel. En suivant ces principes, tu améliores non seulement la fiabilité de ton application, mais tu facilites aussi le travail de ceux qui liront et maintiendront ton code. ✨