Séparer les événements des Effets
Les gestionnaires d’événements ne se réexécutent que lorsque vous réalisez à nouveau 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, a changé depuis le précédent rendu. Parfois, vous souhaitez avoir un comportement hybride : un Effet qui s’exécute à nouveau en réaction à certaines valeurs, mais pas à d’autres. Cette page va vous apprendre à le faire.
Vous allez apprendre
- Comment choisir entre un gestionnaire d’événement et un Effet
- Pourquoi les Effets sont réactifs alors que les gestionnaires d’événements ne le sont pas
- Que faire quand vous voulez qu’une partie du code de votre Effet ne soit pas réactive
- Ce que sont les Événements d’Effets, et comment les extraire de vos Effets
- Comment lire les dernières props et variables d’état à jour depuis vos Effets en utilisant des Événements d’Effets
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émentiez un composant de salon de discussion. Vos besoins sont les suivants :
- Votre composant doit se connecter automatiquement au salon de discussion sélectionné.
- Quand vous cliquez sur le bouton « Envoyer », il doit envoyer un message dans la discussion.
Supposons que vous ayez déjà implémenté le code nécessaire pour ça, mais que vous ne sachiez pas trop 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 réagissent à des interactions spécifiques
Du point de vue de l’utilisateur, l’envoi d’un message doit se faire parce qu’il a cliqué sur le bouton « Envoyer ». L’utilisateur sera plutôt mécontent si vous envoyez son message à un autre moment ou pour une autre raison. C’est pourquoi l’envoi d’un message doit être un gestionnaire d’événement. 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énement, vous pouvez être sûr·e que sendMessage(message)
ne sera exécuté que si l’utilisateur presse 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 laquelle ce code s’exécute n’est pas liée à une interaction particulière. Peu importe pourquoi ou comment l’utilisateur a rejoint le salon de discussion. Maintenant qu’il le voit et peut interagir avec lui, le composant doit rester connecté au serveur sélectionné. Même si ce composant est l’écran initial de votre appli et que l’utilisateur n’a encore rien fait, 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 garantissez qu’il y a toujours une connexion active avec le serveur sélectionné, indépendamment des interactions de l’utilisateur. Que l’utilisateur ait ouvert votre appli, 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 actuellement sélectionné, et se reconnectera chaque fois que 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 la discussion' : 'Ouvrir la discussion'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Valeurs réactives et logique réactive
Intuitivement, vous pourriez penser 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 voir les choses.
Les props, l’état et les variables déclarés au sein de votre composant sont appelés valeurs réactives. Dans cet exemple, serverUrl
n’est pas une valeur réactive, contrairement à roomId
et message
. Ces deux-là participent au flux de données du rendu :
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 nouveau rendu. 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écutera pas à nouveau à moins que l’utilisateur ne répète l’interaction (par exemple un clic). Les gestionnaires d’événements peuvent lire les valeurs réactives sans « réagir » à leurs modifications.
- 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. Par la suite, 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
Regardez 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 simplement parce que la valeur réactive a changé. C’est pourquoi elle a sa place dans le gestionnaire d’événement :
function handleSendClick() {
sendMessage(message);
}
Les gestionnaires d’événements ne sont pas réactifs, de sorte que sendMessage(message)
ne sera exécuté que lorsque l’utilisateur cliquera 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 bien 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écutent à nouveau si la valeur change. C’est pourquoi elle a sa place dans 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 changement de valeur de roomId
. Votre Effet garde la connexion au chat synchronisée avec le salon actuellement sélectionné.
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 souhaitiez afficher une notification quand l’utilisateur se connecte au salon. Vous lisez le thème courant (sombre ou clair) depuis les props de façon à afficher la notification dans 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 nouveau rendu), 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 un problème d’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 dans 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 salon se reconnecte comme prévu. Mais vu que theme
est également une dépendance, le salon se reconnecte aussi à chaque fois que vous passez du thème sombre au thème clair. Ce n’est pas top !
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
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 ce qu’on appelle un Événement d’Effet. Il fait partie de la logique de votre Effet, mais il se comporte davantage comme un gestionnaire d’événement. La logique à l’intérieur n’est pas réactive, et « voit » toujours la dernière valeur à jour de vos props et états.
Vous pouvez désormais appeler l’Événement d’Effet onConnected
depuis 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ésout le problème. Remarquez que vous avez dû retirer onConnected
de la liste des dépendances de votre Effet. Les Événements d’Effets ne sont pas réactifs et ne doivent pas figurer dans 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 dans le salon {roomId} !</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choisissez le salon :{' '} <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’Effets comme étant très similaires aux gestionnaires d’événements. La différence majeure tient à ce que les gestionnaires d’événements réagissent aux interactions de l’utilisateur, alors que les Événements d’Effets sont déclenchés depuis vos Effets. Les Événements d’Effets 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 états à jour avec des Événements d’Effets
Les Événements d’Effets vous permettent de corriger de nombreuses situations où vous seriez tenté·e de réduire le linter de dépendances au silence.
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. Votre composant Page
reçoit désormais une prop url
avec le chemin courant. Vous voulez utiliser url
dans votre appel à logVisit
, mais le linter de dépendances n’est pas content :
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 la recommandation du linter 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, du coup le linter vous demande de l’ajouter comme 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, ça 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.
En revanche, l’Effet lui-même reste réactif. Le code de l’Effet utilise la prop url
, donc l’Effet sera réexécuté après chaque changement de url
que causerait un nouveau rendu. L’Effet appellera à son tour l’Événement d’Effet onVisit
.
Par conséquent, vous appellerez 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.
En détail
Dans les bases de code existantes, vous risquer de tomber sur des désactivations de cette règle du linter, comme ci-dessous :
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Évitez de mettre le *linter* en sourdine 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 réduire le linter au silence.
Désactiver localement cette règle du linter présente un inconvénient majeur : vous empêchez désormais React de vous avertir 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 un linter en sourdine. 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 vient de la mise en sourdine du linter de dépendances. Si vous lui redonnez la parole, vous constaterez que cet Effet doit dépendre de la fonction handleMove
. C’est logique : handleMove
est déclarée au sein 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). React n’ayant pas resynchronisé l’Effet, la fonction handleMove
attachée en tant qu’écouteur d’événement est celle créée au moment du rendu initial. À l’époque canMove
valait true
, c’est pourquoi la fonction handleMove
du rendu initial verra toujours cette valeur-ci.
Si vous écoutez toujours le linter, vous n’aurez jamais de problèmes de valeurs obsolètes.
Avec useEffectEvent
, il est inutile 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 adaptée. 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 Alléger les dépendances des Effets pour d’autres alternatives correctes à la mise en sourdine du linter.
Limitations des Événements d’Effets
Les Événements d’Effets 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’Effets
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
}
Au lieu de ça, déclarez toujours les Événements d’Effets juste à côté 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 dépendance
}
Les Événements d’Effets sont des « parties » non réactives du code de votre Effet. Ils devraient être à côté des Effets qui les utilisent.
En résumé
- Les gestionnaires d’événements réagissent à 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’Effets.
- N’appelez des Événements d’Effets qu’à l’intérieur des Effets.
- Ne transmettez pas les Événements d’Effets à d’autres composants ou Hooks.
Défi 1 sur 4: Restaurer la mise à jour d’une variable
Ce composant Timer
maintient une variable d’état count
qui s’incrémente à chaque seconde. Son pas d’incrément est stocké 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> </> ); }