Accueil / Articles

Choisir l’outil React adapté : Derive, Handle, Fetch, Defer ou Effect

Un guide de décision pour remplacer les appels réflexifs à useEffect par des valeurs dérivées, des gestionnaires d’événements, une couche de données, useTransition, useMemo mesuré et les API de React 19.

Éditeur : REACTAPP.TOP8 min de lecture1896 mots

Sources: Stop Using `useEffect` for Everything: 7 React Patterns That Make Your Code Simpler

De nombreux composants React collectent les appels à useEffect pour effectuer des requêtes, réagir aux props, calculer des valeurs et maintenir l’état en synchronisation. Quelques mois plus tard, vous ouvrez le fichier et découvrez quatre effets, trois variables d’état dérivées, deux indicateurs de chargement ainsi qu’un bug que personne ne parvient à expliquer. Ce guide vous propose une seule question à poser avant d’écrire un hook : « Est-ce que c’est vraiment nécessaire d’utiliser un effet ? », ainsi que sept modèles concrets à appliquer lorsque la réponse est non.

Les valeurs dérivées doivent être dans render, pas dans l’état

L’erreur d’utilisation la plus fréquente consiste à stocker quelque chose qui peut être calculé. Supposons que vous affichiez des produits filtrés par une requête de recherche. Une première tentative courante consiste à ajouter une variable d’état pour le résultat :

const [filteredProducts, setFilteredProducts] = useState([]);

puis un effet qui le recalcule chaque fois que les entrées changent :

useEffect(() => {
  setFilteredProducts(
    products.filter(product =>
      product.name.toLowerCase().includes(query.toLowerCase())
    )
  );
}, [products, query]);

Cela fonctionne, mais filteredProducts n’est pas un état indépendant. C’est une fonction pure des valeurs que vous possédez déjà :

products
+
query
=
filteredProducts

Le stocker signifie que React affiche d’abord des résultats obsolètes, puis exécute l’Effect avant de réafficher. Calculez-le directement dans le corps du composant à la place :

const filteredProducts = products.filter(product =>
  product.name.toLowerCase().includes(query.toLowerCase())
);

Vous n’avez finalement ni variable d’état supplémentaire, ni Effect, rien à synchroniser, et pas de deuxième affichage uniquement pour produire le résultat. Si le filtrage est réellement coûteux, par exemple sur une liste très grande, enveloppez la même expression dans useMemo :

const filteredProducts = useMemo(() => {
  return products.filter(product =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );
}, [products, query]);

Le memo est un détail. Ce qui compte, c’est le principe : une valeur dérivée des props ou de l’état n’a pas besoin d’avoir sa propre place dans l’état.

Les actions de l’utilisateur doivent se trouver dans les gestionnaires d’événements

Un autre schéma dirige un clic via l’état afin qu’un Effect puisse y réagir. D’abord, une flag :

const [shouldSave, setShouldSave] = useState(false);

Ensuite, un Effect qui surveille le drapeau, ainsi qu’un gestionnaire qui le met à jour :

useEffect(() => {
  if (shouldSave) {
    saveUser();
    setShouldSave(false);
  }
}, [shouldSave]);function handleSave() {
  setShouldSave(true);
}

Il n’y a aucune raison pour ce détour. Le clic est le moment où l’action doit avoir lieu, et vous disposez déjà d’une fonction qui s’exécute à ce moment-là :

function handleSave() {
  saveUser();
}

Comparez les deux flux. La version avec le drapeau passe par plusieurs étapes avant que quoi que ce soit ne soit enregistré, puis déclenche une autre mise à l’écran pour se réinitialiser :

Click
  ↓
State update
  ↓
Render
  ↓
Effect
  ↓
saveUser()
  ↓
Another state update
  ↓
Render

La version directe ne comporte qu’une seule étape :

Click
  ↓
saveUser()

La règle générale est que les événements sont gérés dans des gestionnaires d’événements, et que les Effects existent pour synchroniser avec des systèmes externes à React, et non pour dissimuler une logique d’événement ordinaire.

Copiez les props dans l’état uniquement lorsque vous avez besoin de deux valeurs

Celui-ci semble inoffensif. Un composant conserve sa propre copie d’un nom :

const [name, setName] = useState('');

et un Effect écrase cette copie chaque fois que le prop change :

useEffect(() => {
  setName(user.name);
}, [user]);

Il existe maintenant deux sources de vérité, et à tout moment il faut déterminer laquelle est correcte :

user.name
     +
name

Si tout ce que vous faites est d’afficher la valeur, lisez-la directement depuis la propriété :

return <h2>{user.name}</h2>;

Il existe une exception légitime : un formulaire de modification qui conserve délibérément une copie temporaire et modifiable des données serveur jusqu’à ce que l’utilisateur enregistre ou annule :

Server data
    ↓
Edit form
    ↓
User changes fields
    ↓
Save / Cancel

Ici, l’état local est correct car le brouillon constitue réellement une valeur distincte avec son propre cycle de vie. La leçon n’est pas « ne jamais copier les props dans l’état », mais plutôt « comprendre pourquoi on crée une deuxième source de vérité ». Dans le cas des formulaires, réinitialiser le brouillon en attribuant au formulaire une key liée à l’ID du record est souvent plus propre que de synchroniser avec un Effect ; notre article sur la substitution des Effects de synchronisation d’état explique en détail cette technique.

Laissez la couche de données gérer les requêtes

Le chargement des données a tendance à être réparti sur plusieurs Effects. Tout commence par une requête au chargement du composant :

useEffect(() => {
  fetchUser();
}, []);

et évolue pour devenir une série d’Effects, chacun chargant des données pour le même utilisateur :

useEffect(() => {
  fetchOrders();
}, [userId]);useEffect(() => {
  fetchNotifications();
}, [userId]);useEffect(() => {
  fetchPermissions();
}, [userId]);

À ce stade, le composant est devenu une couche d’orchestration API. Dans les applications plus grandes, c’est là que des solutions dédiées à la récupération de données s’avèrent utiles. Selon votre stack technique, il peut s’agir de :

  • TanStack Query
  • SWR
  • les fonctionnalités de récupération de données de votre framework
  • Server Components, là où ils sont adaptés

Sinon, vous devez gérer manuellement chacun de ces aspects pour chaque requête :

loading
error
data
refetch
cache
retry
stale data
race conditions

Une couche de données s’occupe de tout cela à votre place. Conceptuellement, une requête indique quels données elle nécessite et comment les obtenir, puis retourne son état actuel :

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

La clé de la requête sert également de clé de cache, de sorte que les composants demandant le même utilisateur partagent une seule requête. En modifiant userId, on récupère le nouvel utilisateur sans que des réponses obsolètes ne prennent le dessus. Le composant reprend alors la rendu de l’interface utilisateur au lieu de fonctionner comme une petite bibliothèque réseau.

Marquer les mises à jour non urgentes avec useTransition

Imaginons une boîte de recherche qui alimente un tableau coûteux. Avec une seule mise à jour d’état, chaque frappe force le tableau à se rérender avant que l’entrée ne puisse afficher le caractère suivant :

function handleChange(e) {
  setQuery(e.target.value);
}

La solution consiste à diviser la valeur en deux : ce que l’entrée affiche et ce par quoi le tableau filtre :

const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState('');

La valeur de l’entrée est mise à jour normalement, tandis que la mise à jour de la requête est enveloppée dans startTransition:

const [isPending, startTransition] = useTransition();function handleChange(e) {
  const value = e.target.value;  setInputValue(value);  startTransition(() => {
    setQuery(value);
  });
}

React traite désormais ces deux mises à jour différemment. La saisie est considérée comme urgente :

Typing
↓
Urgent

alors que le filtrage et le rérender peuvent être interrompus et retardés :

Filtering/rendering
↓
Can wait

Une transition ne rend pas un code lent plus rapide ; elle modifie le planification afin que l’interface reste réactive et qu’une nouvelle touche puisse interrompre un rendu en cours. isPending vous permet d’indiquer que la table est en train de rattraper son retard.

Mesurez avant de mémoriser

L’échec opposé est l’optimisation par réflexe. Elle commence souvent par un calcul mémorisé :

const result = useMemo(() => {
  return calculateSomething(data);
}, [data]);

suivi d’une mise en correspondance mémorisée :

const users = useMemo(() => {
  return data.map(...);
}, [data]);

puis d’une définition de colonne mémorisée :

const columns = useMemo(() => [...], []);

et d’options mémorisées :

const options = useMemo(() => [...], []);
useMemo aide pour les opérations coûteuses et fournit des références stables pour les enfants mémorisés, mais l’optimisation doit suivre une boucle :

Problem
  ↓
Measure
  ↓
Find bottleneck
  ↓
Optimize
  ↓
Measure again

et non un raccourci :

Component feels complicated
  ↓
Add useMemo

Pour des calculs peu coûteux, la mémorisation ne fait qu’ajouter une charge de lecture et des tableaux de dépendances qui peuvent devenir obsolètes. Le Profilleur des outils de développement React vous indique si un composant est réellement le goulot d’étranglement.

Utilisez les primitives ajoutées par React 19

Plusieurs habitudes des versions antérieures de React disposent désormais de remplacements plus simples. Pendant des années, pour transmettre un ref à son propre composant, il fallait l’envelopper dans forwardRef. Dans React 19, les composants fonctionnels reçoivent ref en tant que propriété ordinaire :

function CustomInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

L’UI optimiste en est un autre exemple. Au lieu d’insérer manuellement un élément temporaire dans l’état et de se souvenir de le supprimer ou de le remplacer plus tard :

setTodos(prev => [
  ...prev,
  {
    id: 'temp',
    text,
    pending: true
  }
]);

vous pouvez utiliser useOptimistic, qui dérive une vue temporaire à partir de l’état réel :

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, todo) => [...state, todo]
);

La valeur optimiste est affichée tant qu’une action asynchrone est en cours et revient à la véritable liste todos une fois celle-ci terminée, de sorte qu’il n’y a aucun élément temporaire à nettoyer. Elle doit être appelée à l’intérieur d’une action ou d’une transition ; les cas limites liés aux demandes échouées sont abordés dans notre article sur les modes d’échec de useOptimistic rollback.

Pour les IDs qui relient des étiquettes à des champs d’entrée en vue de l’accessibilité, utilisez le hook intégré :

const id = useId();

plutôt que de générer un ID au hasard :

const id = `input-${Math.random()}`;

Un ID aléatoire change à chaque rendu et diffère entre le serveur et le client, ce qui perturbe l’hydratation et la liaison avec les étiquettes. React continue d’ajouter des éléments primitifs qui expriment directement l’intention, il convient donc d’éviter de résoudre les problèmes actuels avec des patterns plus anciens.

Liste de vérification avant d’ajouter un hook

Comprendre bien React signifie savoir quand s’abstenir d’utiliser des hooks. Un composant n’est pas forcément meilleur en utilisant tous ces outils :

useEffect()
useMemo()
useCallback()
useRef()
useState()

Visez un flux de données facile à suivre. Avant d’utiliser un hook, définissez le problème et choisissez l’outil adapté.

Pour des données dérivées :

Calculate it.

Pour une action de l’utilisateur :

Handle the event.

Pour synchroniser avec quelque chose en dehors de React, comme une abonnement, un chronomètre ou un widget non React :

Consider useEffect.

Pour lire dans un stockage externe :

Consider useSyncExternalStore or a state library.
Consider useTransition.

Pour un calcul qui pourrait être coûteux en ressources :

Measure first, then consider useMemo.

Et lorsque React dispose déjà d’une fonction primitive pour cette tâche :

Use the primitive instead of rebuilding it yourself.

Points clés

  • Les composants les plus sains ont généralement le moins de fonctions inutiles, et non le plus.
  • Remplacez « Où puis-je utiliser useEffect? » par « Est-ce que ceci a vraiment besoin d’un Effect ? » La plupart du temps, la réponse indique un calcul en temps de rendu, un gestionnaire d’événement ou une bibliothèque de données.
  • Apprenez d’abord le problème que résout chaque nouvelle API React avant de connaître sa syntaxe.
  • Parfois, l’optimisation la plus efficace dans un composant consiste à supprimer une fonction plutôt qu’à en ajouter une.