Séparer les événements des effets

Les gestionnaires d’événements ne s’exécutent à nouveau que lorsque vous réalisez la même interaction. Contrairement aux gestionnaires d’événements, les effets se resynchronisent si une valeur qu’ils lisent, comme une prop ou une variable d’état, est différente de ce qu’elle était lors du précédent affichage. Parfois, vous souhaitez avoir un mélange de ces deux comportements : un effet qui s’exécute à nouveau en réponse à certaines valeurs, mais pas à d’autres. Cette page va vous apprendre à le faire.

Vous allez apprendre

  • Comment choisir entre un gestionnaire d’événements et un effet
  • Pourquoi les effets sont réactifs, et les gestionnaires d’événements non
  • Que faire quand vous voulez qu’une partie du code de votre effet ne soit pas réactive
  • Que sont les événements d’effet et comment les extraire de vos effets
  • Comment lire les derniers props et état des effets en utilisant des événements d’effet

Choisir entre les gestionnaires d’événements et les effets

Tout d’abord, récapitulons la différence entre les gestionnaires d’événements et les effets.

Imaginons que vous implémentez un composant de salon de discussion. Vos exigences sont les suivantes :

  1. Votre composant doit se connecter automatiquement au salon de discussion sélectionné.
  2. Quand vous cliquez sur le bouton « Envoyer », il doit envoyer un message au chat.

Supposons que vous ayez déjà implémenté le code nécessaire pour ça, mais que vous ne soyez pas sûr de savoir où le mettre. Devriez-vous utiliser des gestionnaires d’événements ou des effets ? À chaque fois que vous devez répondre à cette question, réfléchissez à la raison pour laquelle le code doit être exécuté.

Les gestionnaires d’événements s’exécutent en réponse à des interactions spécifiques

Du point de vue de l’utilisateur, l’envoi d’un message doit se faire parce que le bouton « Envoyer » a été cliqué. L’utilisateur sera plutôt mécontent si vous envoyez leur message à un autre moment ou pour une autre raison. C’est pourquoi l’envoi d’un message doit être un gestionnaire d’événements. Les gestionnaires d’événements vous permettent de gérer des interactions spécifiques :

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Envoyer</button>;
</>
);
}

Avec un gestionnaire d’événements, vous êtes assuré que sendMessage(message) ne sera exécuté que si l’utilisateur appuie sur le bouton.

Les effets s’exécutent à chaque fois qu’une synchronisation est nécessaire

Rappelez-vous que vous devez également veiller à ce que le composant reste connecté au salon de discussion. Où va ce code ?

La raison pour exécuter ce code n’est pas liée à une interaction particulière. Peu importe pourquoi ou la façon dont l’utilisateur a rejoint le salon de discussion. Maintenant qu’il le voit et peut interagir avec, le composant doit resté connecté au serveur de chat sélectionné. Même si le composant de salon de discussion est l’écran initial de votre app, et que l’utilisateur n’a fait aucune interaction, vous devrez tout de même vous connecter. C’est pourquoi il s’agit d’un effet :

function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

Avec ce code, vous pouvez être sûr qu’il y a toujours une connexion active avec le serveur de chat sélectionné, indépendamment d’une quelconque interaction de l’utilisateur. Que l’utilisateur ait ouvert votre app, sélectionné un autre salon ou navigué vers un autre écran avant d’en revenir, votre effet garantit que le composant reste synchronisé avec le salon sélectionné actuellement, et pourra se reconnecter chaque fois que ça sera nécessaire.

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  function handleSendClick() {
    sendMessage(message);
  }

  return (
    <>
      <h1>Bievenue dans le salon {roomId} !</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleSendClick}>Envoyer</button>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choisissez le salon de discussion :{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">général</option>
          <option value="travel">voyage</option>
          <option value="music">musique</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Fermer le chat' : 'Ouvrir le chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Valeurs réactives et logique réactive

Intuitivement, vous pourriez dire que les gestionnaires d’événements sont toujours déclenchés « manuellement », par exemple en cliquant sur un bouton. Les effets, quant à eux, sont « automatiques » : ils sont exécutés et réexécutés aussi souvent que nécessaire pour rester synchronisés.

Il y a une façon plus précise de penser à ça.

Les props, l’état et les variables déclarés à l’intérieur du corps de votre composant sont appelés valeurs réactives. Dans cet exemple, serverUrl n’est pas une valeur réactive, mais roomId et message le sont. Elles participent au flux de données de l’affichage :

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

// ...
}

Les valeurs réactives comme celles-ci peuvent changer à la suite d’un réaffichage. Par exemple, l’utilisateur peut éditer le message ou choisir un roomId différent depuis une liste déroulante. Les gestionnaires d’événements et les effets réagissent différemment à ces changements :

  • La logique au sein des gestionnaires d’événements n’est pas réactive. Elle ne s’exécutéra pas à nouveau à moins que l’utilisateur réalise la même interaction (par exemple un clic).
  • La logique au sein des effets est réactive. Si votre effet lit une valeur réactive, vous devez la spécifier en tant que dépendance. Ensuite, si un nouveau rendu entraîne un changement de cette valeur, React réexécutera la logique de votre effet avec la nouvelle valeur.

Reprenons l’exemple précédent pour illustrer cette différence.

La logique à l’intérieur des gestionnaires d’événements n’est pas réactive

Jetez un œil à cette ligne de code. Cette logique doit-elle être réactive ou non ?

// ...
sendMessage(message);
// ...

Du point de vue de l’utilisateur, un changement de message ne signifie pas qu’il souhaite envoyer un message. Ça signifie seulement que l’utilisateur est en train de taper. En d’autres termes, la logique qui envoie un message ne doit pas être réactive. Elle ne doit pas s’exécuter à nouveau parce que la valeur réactive a changé. C’est pourquoi elle appartient au gestionnaire d’événéments :

function handleSendClick() {
sendMessage(message);
}

Les gestionnaires d’événements ne sont pas réactifs, ainsi sendMessage(message) ne sera exécuté que lorsque l’utilisateur clique sur le bouton Envoyer.

La logique à l’intérieur des effets est réactive

Maintenant, revenons à ces lignes :

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...

Du point de vue de l’utilisateur, un changement de roomId signifie qu’il veut se connecter à un salon différent. En d’autres termes, la logique de connexion à un salon doit être réactive. Vous voulez que ces lignes de code « suivent » la valeur réactive, et s’exécute à nouveau si la valeur est différente. C’est pourquoi elle appartient à un effet :

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);

Les effets sont réactifs, donc createConnection(serverUrl, roomId) et connection.connect() s’exécuteront pour chaque valeur distincte de roomId. Votre effet garde la connexion au chat synchronisée avec le salon sélectionné actuellement.

Extraire la logique non réactive des effets

Les choses deviennent plus compliquées quand vous souhaitez mélanger une logique réactive avec une logique non réactive.

Par exemple, imaginez que vous souhaitez afficher une notification quand l’utilisateur se connecte au chat. Vous lisez le thème courant (sombre ou clair) depuis les props de façon à afficher la notification avec la bonne couleur :

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connecté·e !', theme);
});
connection.connect();
// ...

Cependant, theme est une valeur réactive (elle peut changer à la suite d’un nouvel affichage), et chaque valeur réactive lue par un effet doit être déclarée dans ses dépendances. Vous devez maintenant spécifier theme comme une dépendance de votre effet :

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connecté·e !', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ Toutes les dépendances sont déclarées.
// ...

Jouez avec cet exemple et voyez si vous identifiez le problème avec cette expérience utilisateur :

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connecté·e !', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Bienvenue sur le salon {roomId} !</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choisissez le salon de discussion :{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">général</option>
          <option value="travel">voyage</option>
          <option value="music">musique</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Utiliser le thème sombre
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

Quand roomId change, le chat se reconnecte comme on peut s’y attendre. Mais comme theme est également une dépendance, le chat se reconnecte aussi à chaque fois que vous passez du thème sombre au thème clair. Ce n’est pas génial !

En d’autres termes, vous ne voulez pas que cette ligne soit réactive, même si elle se trouve dans un effet (qui est réactif) :

// ...
showNotification('Connecté·e !', theme);
// ...

Vous devez trouver une façon de séparer cette logique non réactive de l’effet réactif qui l’entoure.

Déclarer un événement d’effet

En construction

Cette section décrit une API expérimentale qui n’a pas encore été livrée dans une version stable de React.

Utilisez un Hook spécial appelé useEffectEvent pour extraire cette logique non réactive de votre effet :

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connecté·e !', theme);
});
// ...

Ici, onConnected est appelé un événement d’effet. Il fait partie de la logique de votre effet, mais il se comprend bien plus comme un gestionnaire d’événements. La logique à l’intérieur n’est pas réactive, et « voit » toujours la dernière valeur de vos props et état.

Maintenant vous pouvez appeler l’événement d’effet onConnected à l’intérieur de votre effet :

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connecté·e !', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Toutes les dépendances sont déclarées.
// ...

Ça résoud le problème. Remarquez que vous avez dû supprimer onConnected de la liste des dépendances de votre effet. Les événements d’effet ne sont pas réactifs et doivent être omis de vos dépendances.

Vérifiez que le nouveau comportement fonctionne comme attendu :

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connecté·e !', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Bienvenue sur le salon {roomId} !</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choisissez le salon de chat :{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">général</option>
          <option value="travel">voyage</option>
          <option value="music">musique</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Utiliser le thème sombre
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

Vous pouvez considérer les événements d’effet comme étant très similaires aux gestionnaires d’événements. La différence majeure est que les gestionnaires d’événements s’exécutent en réponse aux interactions de l’utilisateur, alors que les événements d’effet sont déclenchés par vos effets. Les événements d’effet vous permettent de « briser la chaîne » entre la réactivité des effets et le code qui ne doit pas être réactif.

Lire les dernières props et état avec des événements d’effet

En construction

Cette section décrit une API expérimentale qui n’a pas encore été livrée dans une version stable de React.

Les événements d’effet vous permettent de corriger de nombreuses situations où vous seriez tentés de supprimer le linter de dépendance.

Par exemple, disons que vous avec un effet qui enregistre les visites de la page :

function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}

Plus tard, vous ajoutez plusieurs routes à votre site. Maintenant, votre composant Page reçoit une prop url avec le chemin courant. Vous voulez passer url comme une partie de votre appel à logVisit, mais le linter de dépendance se plaint :

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 Le Hook React useEffect a une dépendance manquante : 'url'.
// ...
}

Réfléchissez à ce que vous voulez que le code fasse. Vous souhaitez enregistrer une visite différente pour des URL différentes, puisque chaque URL représente une page différente. En d’autres termes, cet appel à logVisit doit être réactif par rapport à url. C’est pourquoi, dans ce cas, il est logique de suivre le linter de dépendance et d’ajouter url comme dépendance :

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ Toutes les dépendances sont déclarées.
// ...
}

Supposons maintenant que vous vouliez inclure le nombre d’articles du panier d’achat à chaque visite :

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 Le Hook React useEffect a une dépendance manquante : 'numberOfItems'.
// ...
}

Vous avez utilisé numberOfItems dans votre effet, aussi le linter vous demande de l’ajouter comme une dépendance. Cependant, vous ne voulez pas que l’appel à logVisit soit réactif par rapport à numberOfItems. Si l’utilisateur place quelque chose dans le panier d’achat et que numberOfItems change, cela ne signifie pas que l’utilisateur a visité la page à nouveau. En d’autres termes, visiter la page est, en quelque sorte, un « événement ». Il se produit à un moment précis.

Séparez le code en deux parties :

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ Toutes les dépendances sont déclarées
// ...
}

Ici, onVisit est un événement d’effet. Le code à l’intérieur n’est pas réactif. C’est pourquoi vous pouvez utiliser numberOfItems (ou n’importe quelle valeur réactive !) sans craindre que le code environnant ne soit réexécuté après un changement.

D’un autre côté, l’effet lui-même reste réactif. Le code à l’intérieur de l’effet utilise la prop url, donc l’effet sera réexécuté après chaque réaffichage avec une url différente. Ça appelera à son tour l’événement d’effet onVisit.

Par conséquent, vous appelerez logVisit pour chaque changement d’url et lirez toujours la dernière valeur de numberOfItems. Cependant, si numberOfItems change à son tour, ça ne causera aucune réexécution de code.

Remarque

Vous vous demandez peut-être si vous pouvez appeler onVisit() sans paramètres et de lire l’url à l’intérieur :

const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});

useEffect(() => {
onVisit();
}, [url]);

Ça fonctionnerait, mais il est préférable de passer cette url explicitement à l’événement d’effet. En passant url comme paramètre à votre événement d’effet, vous dites que la visite d’une page avec une url différente constitue un « événement » d’un point de vue de l’utilisateur. Le visitedUrl fait partie de l’« événement » qui s’est produit :

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]);

Puisque votre événement d’effet « demande » explicitement le visitedUrl, vous ne pouvez plus supprimer accidentellement url des dépendances de votre effet. Si vous supprimez la dépendance url (ce qui fait que des visites de plusieurs pages distinctes sont comptées comme une seule visite), le linter vous en avertira. Vous voulez que onVisit soit réactif par rapport à url, donc plutôt que lire url à l’intérieur (où il ne serait pas réactif), vous le transmettez à partir de votre effet.

Ça devient particulièrement important lorsqu’il y a une logique asynchrone à l’intérieur de l’effet :

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // Décalage de l’enregistrement des visites.
}, [url]);

Ici, url à l’intérieur de onVisit correspond à la dernière url (qui a pu changé depuis), mais visitedUrl correspond à l’url qui a déclenché l’exécution de l’effet à l’origine (et donc de l’appel à onVisit).

En détail

Est-il acceptable de plutôt supprimer le linter de dépendance ?

Dans les bases de code existantes, vous pouvez voir parfois la règle du linter supprimée de cette façon :

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Évitez de supprimer le linter comme ça :
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}

Dès que useEffectEvent sera devenu une partie stable de React, nous recommanderons de ne jamais supprimer le linter.

Le premier inconvénient de la suppression de la règle est que React ne vous avertira plus quand votre effet doit « réagir » à une nouvelle dépendance réactive que vous avez introduite dans votre code. Dans l’exemple précédent, vous avez ajouté url aux dépendances parce que React vous l’a rappelé. Vous n’aurez plus de tels rappels pour vos prochaines modifications de cet effet si vous désactivez le linter. Ça entraîne des bugs.

Voici un exemple d’un bug déroutant causé par la suppression du linter. Dans cet exemple la fonction handleMove est supposée lire la valeur actuelle de la variable d’état canMove afin de décider si le point doit suivre le curseur. Cependant, canMove est toujours à true à l’intérieur de handleMove.

Voyez-vous pourquoi ?

import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  function handleMove(e) {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }

  useEffect(() => {
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        Le point peut se déplacer
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

Le problème avec ce code tient en la suppression du linter de dépendance. Si vous enlevez cette suppression, vous constaterez que cet effet doit dépendre de la fonction handleMove. C’est logique : handleMove est déclarée à l’intérieur de corps du composant, ce qui en fait une valeur réactive. Toute valeur réactive doit être spécifiée en tant que dépendance, sans quoi elle pourrait devenir obsolète par la suite !

L’auteur du code d’origine a « menti » à React en disant que l’effet ne dépend ([]) d’aucune valeur réactive. C’est pourquoi React n’a pas resynchronisé l’effet après que canMove a changé (et handleMove avec elle). Parce que React n’a pas resynchronisé l’effet, la fonction handleMove attachée en tant qu’écouteur est la fonction handleMove créée au moment de l’affichage initial. Lors de cet affichage initial, canMove valait true, c’est pourquoi la fonction handleMove de l’affichage initial verra toujours cette valeur.

Si vous supprimez le linter, vous ne verrez jamais les problèmes avec les valeurs obsolètes.

Avec useEffectEvent, il n’est pas utile de « mentir » au linter et le code fonctionne comme prévu :

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        Le point peut se déplacer
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

Ça ne signifie pas que useEffectEvent soit toujours la solution correcte. Dans le bac à sable ci-dessus, vous ne vouliez pas que le code de l’effet soit réactif par rapport à canMove. C’est pourquoi il était logique d’extraire un événement d’effet.

Lisez Supprimer les dépendances des effets pour d’autres alternatives correctes au retrait du linter.

Limitations des effets d’événements

En construction

Cette section décrit une API expérimentale qui n’a pas encore été livrée dans une version stable de React.

Les événements d’effet sont très limités dans leur utilisation :

  • Ne les appelez qu’à l’intérieur des effets.
  • Ne les transmettez jamais à d’autres composants ou Hooks.

Par exemple, ne déclarez pas et ne transmettez pas un événement d’effet ainsi :

function Timer() {
const [count, setCount] = useState(0);

const onTick = useEffectEvent(() => {
setCount(count + 1);
});

useTimer(onTick, 1000); // 🔴 À éviter : transmettre des événements d’effet.

return <h1>{count}</h1>
}

function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Il est nécessaire de déclarer "callback" dans les dépendances.
}

À la place, déclarez toujours les événements d’effet directement à proximité des effets qui les utilisent :

function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}

function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});

useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Correct : appelé uniquement à l’intérieur d’un effet.
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // Il est inutile de spécifier "onTick" (un événement d’effet) comme une dépendance.
}

Les événements d’effet sont des « parties » non réactives du code de votre effet. Elles doivent être à proximité des effets qui les utilisent.

En résumé

  • Les gestionnaires d’événements sont exécutés en réponse à des interactions spécifiques.
  • Les effets sont exécutés à chaque fois qu’une synchronisation est nécessaire.
  • La logique au sein des gestionnaires d’événements n’est pas réactive.
  • La logique contenue dans les effets est réactive.
  • Vous pouvez déplacer de la logique non réactive des effets vers des événements d’effet.
  • Vous ne devez appeler des événements d’effet qu’à l’intérieur des effets.
  • Ne transmettez pas les événements d’effet à d’autres composants ou Hooks.

Défi 1 sur 4:
Corriger une variable qui ne se met pas à jour

Ce composant Timer conserve une variable d’état count qui s’incrémente à chaque seconde. La valeur par laquelle elle s’incrémente est stockée dans la variable d’état increment. Vous pouvez contrôler la variable increment avec les boutons plus et moins.

Cependant, peu importe combien de fois vous cliquez sur le bouton plus, le compteur est toujours incrémenté d’une unité à chaque seconde. Qu’est-ce qui ne va pas dans ce code ? Pourquoi increment vaut-il toujours 1 à l’intérieur du code de l’effet ? Trouvez l’erreur et corrigez-la.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Compteur : {count}
        <button onClick={() => setCount(0)}>Réinitialiser</button>
      </h1>
      <hr />
      <p>
        Chaque seconde, incrémenter de :
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}