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 :
- Votre composant doit se connecter automatiquement au salon de discussion sélectionné.
- 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
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
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.
En détail
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
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> </> ); }