# Un useSelect mal placé qui provoque une boucle de rendu infinie

> L'éditeur devient poussif, le ventilateur du portable s'emballe : un useSelect non mémoïsé dans le corps d'un composant peut déclencher un cycle de rendu sans fin.

- Auteur : Clément Hadrot
- Publié le : 2024-02-14
- Mis à jour le : 2024-02-14
- Catégorie : Blocs Gutenberg
- URL : https://wpmoderne.dev.wordpress-developpement.fr/blocs/useselect-mal-place-boucle-rendu-infinie/

## L’essentiel

- Un objet ou tableau recréé à chaque rendu casse la comparaison
- Toujours retourner des primitives ou des références stables
- React DevTools Profiler pour repérer la boucle

L'alerte est venue d'une rédactrice du magazine en ligne d'un client de Kaolin : « l'éditeur rame, l'ordinateur chauffe, même sans rien taper ». Le premier réflexe a été de suspecter une extension tierce ou un plugin de cache mal configuré. Le vrai coupable était un bloc « articles liés » installé deux semaines plus tôt, dont le composant d'édition contenait un `useSelect` mal écrit.

Ce type de bug est particulièrement traître : il ne provoque aucune erreur dans la console, aucun message rouge. L'éditeur continue de fonctionner, juste de plus en plus lentement, jusqu'à devenir inutilisable sur les pages les plus longues.

## Symptôme

Le bloc en question affichait une liste des trois derniers articles de la même catégorie que l'article en cours. Rien d'inhabituel visuellement : la liste s'affichait correctement, mais le processeur du navigateur restait bloqué à 100 % tant que l'éditeur était ouvert. Le Gestionnaire des tâches de Chrome montrait l'onglet de l'admin WordPress consommant plus de ressources qu'un onglet de lecture vidéo.

## Diagnostic avec React DevTools Profiler

Le Profiler de React DevTools, activé sur l'onglet de l'éditeur, a montré un enregistrement saturé de rendus du même composant, plusieurs centaines par seconde, sans qu'aucune interaction utilisateur ne les déclenche. Le code du bloc contenait ceci :

```
function Edit({ attributes }) {
  const { categorieId } = attributes;

  const articlesLies = useSelect((select) => {
    return select('core').getEntityRecords('postType', 'post', {
      categories: [categorieId],
      exclude: [wp.data.select('core/editor').getCurrentPostId()],
      per_page: 3,
    });
  }, [categorieId]);

  return ( /* ... */ );
}
```

> L'essentiel à retenir : Un objet ou tableau recréé à chaque rendu casse la comparaison ; Toujours retourner des primitives ou des références stables ; React DevTools Profiler pour repérer la boucle

Le tableau `exclude: [wp.data.select('core/editor').getCurrentPostId()]` est recréé à chaque exécution du sélecteur, avec une nouvelle référence à chaque fois, même si l'identifiant de l'article ne change jamais. Or `getEntityRecords` traite les paramètres de requête comme faisant partie de la clé de cache : une nouvelle référence de tableau, même avec un contenu identique, est interprétée comme une requête différente. Le store `core` déclenche donc un nouveau chargement, ce qui déclenche un nouveau rendu, qui recrée le tableau, qui déclenche un nouveau chargement, indéfiniment.

## Pourquoi le tableau de dépendances ne protégeait pas

Le tableau de dépendances passé à `useSelect`, `[categorieId]`, ne contenait que l'identifiant de catégorie, qui ne changeait pas. Mais ce tableau ne protège que contre le re-calcul du *sélecteur lui-même* côté React ; il n'empêche pas `getEntityRecords` de recevoir un objet de requête différent en interne à chaque appel, puisque cet objet est construit dans le corps de la fonction passée à `useSelect`, exécutée à chaque rendu du composant.

## Correctif

La correction consiste à sortir l'identifiant de l'article courant du tableau de requête, en le récupérant une seule fois à l'extérieur de la fonction de sélection, ou à défaut à s'assurer que la structure de la requête reste strictement identique entre deux rendus :

```
function Edit({ attributes }) {
  const { categorieId } = attributes;
  const idArticleCourant = useSelect(
    (select) => select('core/editor').getCurrentPostId(),
    []
  );

  const articlesLies = useSelect((select) => {
    if (!idArticleCourant) return null;
    return select('core').getEntityRecords('postType', 'post', {
      categories: [categorieId],
      exclude: [idArticleCourant],
      per_page: 3,
    });
  }, [categorieId, idArticleCourant]);

  return ( /* ... */ );
}
```

Le tableau `exclude` reste recréé à chaque rendu, mais son contenu ne varie plus tant que `idArticleCourant` ne change pas : la requête générée est strictement identique d'un rendu à l'autre, et `getEntityRecords` retourne la donnée en cache sans redéclencher de requête.

## Prévention

- Ne jamais appeler un autre `select()` imbriqué à l'intérieur d'un sélecteur déjà en cours d'exécution sans le sortir dans un `useSelect` séparé.
- Se méfier de tout tableau ou objet littéral construit directement dans les paramètres d'une requête à une entité.
- Ouvrir le Profiler React DevTools au moindre ralentissement inexpliqué de l'éditeur : une boucle de rendu s'y voit immédiatement sous forme de bande de rendus continue.

> Une requête aux entités WordPress doit toujours pouvoir être comparée trait pour trait au rendu précédent, sans quoi le cache ne sert à rien.

## Notre verdict

Ce genre de boucle ne se voit pas dans une revue de code rapide : le tableau de dépendances de `useSelect` paraît correct, l'erreur se cache dans la construction de la requête elle-même. Sur ce projet, le correctif a fait chuter la consommation processeur de l'onglet éditeur de plus de 400 rendus par seconde à un rendu unique au chargement du bloc.
