Accueil / Articles / Une liste de contrôle pour l’évaluation du code React : état dérivé, réducteurs, Effects et Memo

Une liste de contrôle pour l’évaluation du code React : état dérivé, réducteurs, Effects et Memo

Apprenez à repérer sept problèmes courants lors de l’évaluation de React, allant de l’état dupliqué et des requêtes gérées manuellement à une utilisation incorrecte d’effects et d’une mémorisation excessive, ainsi que ce qu’il convient d’écrire à la place.

1906 mots

Les développeurs ayant quelques années d’expérience avec React savent généralement comment fonctionnent les props, comment agissent les hooks et pourquoi l’état ne doit jamais être modifié directement. Pourtant, les revues de code révèlent constamment les mêmes problèmes dans leurs demandes de fusion : des composants qui fonctionnent mais sont fragiles, difficiles à tester, surconstruits ou peu extensibles. Dépasser cette étape ne consiste pas tant à apprendre des API obscures qu’à affiner la compréhension de l’endroit où se trouve l’état, à quelles fins servent les effets et quand l’optimisation est utile. Cette liste de contrôle passe en revue sept problèmes récurrents, en utilisant une liste à faire en TypeScript comme exemple concret, et montre le modèle à adopter dans chaque cas.

1. État qui devrait être dérivé

La règle fondamentale de l’état dans React est de conserver une seule source de vérité. Chaque fois que les mêmes informations se trouvent en deux endroits, les copies finiront par être incohérentes.

À quoi cela ressemble

Une liste de tâches qui doit conserver à la fois toutes les tâches et celles déjà terminées se retrouve souvent avec deux tableaux dans l’état :

// ❌ BAD: Storing derived state in two separate places
const [todos, setTodos] = useState<Todo[]>([]);
const [completedTodos, setCompletedTodos] = useState<Todo[]>([]);

Désormais, chaque modification doit être appliquée deux fois. Si un utilisateur renomme une tâche et que le code met à jour todos mais oublie completedTodos, l’interface affiche le titre ancien dans une liste et le nouveau dans l’autre.

Que faire à la place

Conserver un seul tableau dans l’état et calculer le sous-ensemble des tâches terminées lors du rendu :

// ✅ GOOD: Single source of truth with derived state
const [todos, setTodos] = useState<Todo[]>([]);
// Derived value recalculated on every render (inexpensive computation)
const completedTodos = todos.filter((todo) => todo.completed);

Le filtrage d’un tableau à chaque rendu est peu coûteux pour des tailles typiques, et la liste dérivée ne peut jamais être désynchronisée car elle est recalculée à partir de la source à chaque fois. Si le calcul devient réellement coûteux, par exemple pour filtrer des dizaines de milliers d’éléments, l’envelopper dans useMemo ; sinon, une expression simple est plus claire.

2. Récupération manuelle des données dans useEffect

Les appels manuels à fetch à l’intérieur de useEffect semblent simples, mais ils ont tendance à générer des conditions de concurrence, ne disposent pas de mécanisme de mise en cache et exigent que l’état de chargement ainsi que les erreurs soient gérés manuellement dans chaque composant.

À quoi cela ressemble

Cet effet charge la liste une seule fois et utilise un drapeau isMounted pour s’assurer qu’il ne modifie pas l’état après le démontage du composant :

// ❌ BAD: Boilerplate-heavy, error-prone, no caching or deduplication
useEffect(() => {
  let isMounted = true;
  fetch('/api/todos')
    .then((res) => res.json())
    .then((data) => {
      if (isMounted) setTodos(data);
    });
  return () => { isMounted = false; };
}, []);

Il reste cependant des lacunes : il n’y a ni gestion des erreurs, ni état de chargement, ni suppression des doublons lorsque deux composants demandent les mêmes données, ni mise en cache entre les écrans, ni possibilité de tentative répétée. Le drapeau isMounted ne fait que supprimer la mise à jour tardive ; il ne cancelle pas la requête, ce que ferait un AbortController.

Que mettre à la place

Utilisez une bibliothèque conçue pour gérer l’état du serveur, telle que TanStack Query, SWR ou RTK Query. L’appel équivalent de TanStack Query définit une clé de cache et une fonction de récupération, puis reçoit les données ainsi que l’état de chargement et d’erreur :

// ✅ GOOD: Declarative, automatically cached, auto-retried
const { data: todos, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

De telles bibliothèques gèrent automatiquement la révalidation en arrière-plan, la suppression des requêtes redondantes, les tentatives de récupération et le partage du cache. Elles offrent également un soutien structuré pour les mises à jour optimistes et l’intégration avec des limites d’erreur. Le code du composant se réduit alors à une description de ses besoins.

3. Une série d’appels à useState alors qu’un réducteur suffirait

useState est idéal pour des valeurs simples comme un interrupteur ou un champ de texte. Lorsque les mises à jour dépendent de valeurs précédentes, ou lorsque l’état comprend des tableaux et des objets imbriqués modifiés par plusieurs actions différentes, l’utilisation de multiples fonctions d’ajustement rend les composants encombrés et favorise l’apparition de bugs liés à des états obsolètes.

À quoi cela ressemble

Un seul composant déclare cinq à dix hooks useState et les met à jour depuis de nombreux callbacks pour ajouter, modifier, commuter ou supprimer des éléments. Les règles métier finissent par être dispersées dans les gestionnaires d’événements.

Que mettre à la place

useReducer déplace les transitions d’état hors du composant vers une seule fonction. Le réducteur ci-dessous utilise une union discriminée pour ses actions, ce qui permet à TypeScript de connaître le type du payload pour chaque cas. L’ajout d’une tâche insère un nouvel objet avec un id généré ; le commutateur parcourt la liste et inverse l’état de l’élément correspondant. Notez que la ligne du commutateur contient une faute d’orthographe : ...todo.completed devrait être !todo.completed, qui représente la négation logique, sinon le code ne se compile pas.

type Action =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string };

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: crypto.randomUUID(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, completed: ...todo.completed } : todo
      );
    default:
      return state;
  }
}

Les réducteurs présentent deux avantages :

  • Testabilité. Un réducteur étant une fonction pure, vous pouvez le tester sans afficher quoi que ce soit ni simuler React.
  • Predictibilité. Chaque mise à jour passe par une action nommée plutôt que par des fonctions de mise à jour arbitraires appelées depuis des callbacks dispersés, ce qui facilite le suivi et l’enregistrement des modifications.

4. Aucun test parce que « le testing est un projet complexe »

De nombreuses équipes considèrent le testing comme quelque chose d’extrême : soit une configuration complète de type bout en bout, qui semble intimidante, soit rien du tout. Une approche plus judicieuse consiste à commencer par les tests qui sont les plus faciles à écrire et les plus utiles à conserver.

Priorisez dans cet ordre :

  • Logique métier indépendante. Auxiliaires, formatteurs, validateurs et outils qui se trouvent en dehors de l’arbre des composants.
  • Réducteurs et hooks personnalisés. Logique de transition d’état pure.
  • Journeys utilisateur critiques. Les interactions clés, testées avec React Testing Library (@testing-library/react).
  • Un test de réducteur constitue une bonne première étape. Il crée un état initial, envoie une action directement via la fonction et vérifie le résultat, sans aucune mise en rendu :

    // Example: Testing a reducer takes 2 minutes and provides huge peace of mind
    test('should toggle todo status', () => {
      const initialState = [{ id: '1', text: 'Write Article', completed: false }];
      const newState = todoReducer(initialState, { type: 'TOGGLE_TODO', payload: '1' });
    
      expect(newState[0].completed).toBe(true);
    });
    

    Un tel test prend plusieurs minutes à écrire et protège les règles sur lesquelles tout le fonctionnement dépend. Une fois la faute de frappe dans le réducteur mentionnée précédemment corrigée, c’est exactement ce test qui l’aurait détectée.

    5. Utilisation de useEffect pour synchroniser React avec lui-même

    useEffect est le hook le plus mal utilisé dans React. Un schéma fréquent consiste à l’utiliser pour copier ou ajuster l’état interne lorsque une propriété change, ce qui provoque un rendu supplémentaire et un bref moment d’UI incohérente.

    Deux idées fausses reviennent constamment :

    • « L’état doit être mis à jour dans un effet lorsque une propriété change. » Généralement non. Calculez la valeur lors du rendu, ou, si l’état d’un enfant doit être réinitialisé, donnez-lui une key qui change avec la propriété concernée afin que React le rémonte.
    • « Le comportement de clic doit se trouver dans un effet. » La logique déclenchée par une action de l’utilisateur doit être placée dans le gestionnaire d’événements, comme onClick, et non dans un effet qui surveille le changement d’état provoqué par le clic.

    Le principe directeur : les effets servent de solution pour synchroniser React avec des systèmes externes, tels que les API du navigateur, les abonnements, les outils d’analyse ou des widgets DOM de tiers. Les données déjà gérées par React peuvent être transformées sans recourir à ces effets. Pour une explication plus détaillée accompagnée d’exemples avant/après, consultez arrêter la synchronisation de l’état avec useEffect.

    6. Mémoriser tout par défaut

    Les efforts de performance tournent souvent entre deux extrêmes : ignorer complètement le coût de rendu, ou envelopper chaque composant dans React.memo et chaque fonction dans useCallback.

    Pourquoi la mémorisation systématique a des effets contraires

    useMemo et useCallback ne sont pas gratuits. React doit stocker les dépendances et les comparer à chaque rendu. Pour un composant dont le rendu est rapide, cette gestion peut coûter autant que le rendu qu’il tente d’éviter, et cela rend le code plus difficile à lire. La mémorisation ne s’avère utile que lorsque toutes les propriétés sont stables ; un objet inline ou une fonction flèche transmise à un enfant mémorisé annule son effet.

    Une approche plus réfléchie

    • Profilez d’abord. Le Profiler des outils de développement React indique quels composants sont effectivement rendus trop souvent ou trop lentement.
    • Réglez la structure avant d’ajouter de la mémorisation. Déplacer l’état dans le composant qui l’utilise, ou transmettre des sous-arbres coûteux en tant que children, permet souvent d’éviter des rendus inutiles sans aucune mémorisation.
  • Mémorisez là où c’est important. Appliquez React.memo aux listes longues et aux graphiques gourmands, et maintenez leurs props stables à l’aide de useCallback et useMemo.
  • 7. Considérer le CSS et l’accessibilité comme du travail d’autrui

    Les ingénieurs React compétents maîtrisent également bien la plateforme navigateur. Les composants ne sont bons que dans la mesure où le markup et les styles qu’ils génèrent le sont aussi.

    Domaines à maîtriser :

    • Agencement moderne. Maîtrisez suffisamment Flexbox et CSS Grid pour que la position absolue ne soit plus le recours par défaut pour les agencements ordinaires.
    • Styling ciblé. Gardez les styles à côté des composants grâce aux CSS Modules, Tailwind CSS ou CSS-in-JS, plutôt que de compter sur de grandes feuilles de style globales où les noms de classes entrent en conflit.
    • Accessibilité et utilisabilité. Utilisez des éléments sémantiques, tels qu’un véritable <button> plutôt que <div onClick={...}>. Assurez-vous que la navigation au clavier avec Tab, Entrée et Espace fonctionne partout. Prévoyez des styles :focus-visible visibles ainsi que des états interactifs clairs, comme un curseur pointeur sur les éléments cliquables.

    Liste de contrôle pour l’évaluation

    Lors de l’examen d’une demande de pull request en React, cherchez ces substitutions :

    • Valeurs dérivées stockées dans des hooks useState supplémentaires : calculez-les plutôt pendant le rendu.
    • Appels API manuels à l’intérieur de useEffect : utilisez TanStack Query, SWR ou RTK Query.
    • De nombreux appels à useState pour des tableaux et des objets complexes : déplacez les transitions dans useReducer et testez le réducteur.
    • Aucun test, car la configuration semble coûteuse : commencez avec des fonctions pures, des reducers et des hooks personnalisés.
    • useEffect utilisé pour synchroniser l’état : dérivez la valeur ou gérez-la dans le gestionnaire d’événements.
    • useCallback et React.memo partout : analysez les performances avant de mettre en mémoire.
    • Éléments div cliquables et styles de focus manquants : utilisez du HTML sémantique et vérifiez l’accès par clavier.

    En résumé

    La plupart de ces problèmes ont une cause commune : un état ou une logique qui existe en plusieurs endroits ou s’exécute plus souvent que nécessaire. En conservant une seule source de vérité, en laissant des outils conçus spécifiquement gérer les données serveur, en isolant les transitions dans des fonctions pures et en réservant les effets au monde extérieur, on réduit le nombre de composants dynamiques. Un code avec moins de composants dynamiques est plus facile à tester, plus facile à optimiser lorsque c’est nécessaire et plus facile pour le prochain auditeur de lui faire confiance.