Nouvelle série d’articles
Nouvelle série d’articles baptisés « Quicky ». Ils portent ce nom car à priori, ils devraient être court 🙂 J’utiliserai aussi mon repository github où vous pourrez voir un code complet avec des commits pour chaque étape de refactoring… Des commentaires dans le code seront rajoutés pour bien montrer l’avant et l’après à chaque commit
TL;DR :
- Evitez les useEffect en cascade (un useEffect qui va modifier un state qui va faire en sorte d’appeler un autre useEffect, etc…)
- useMemo peut faire le job à la place
- Bonus: Appeler plusieurs setState dans une méthode asynchrone n’est pas une bonne idée 🙂
Trop de useEffect
Voici un exemple de code qui me dit que parfois, les useEffect, c’est vraiment puant… Un useEffect qui « appelle » un autre
useEffect. C’est à devenir fou! Par exemple, cet exemple, où une liste « async » qui, une fois chargée change certains compteurs via un deuxième useEffect. Pour les besoin de la démo, le code a été simplifié et « bien sûr » que personne ne code comme cela, n’est-ce pas?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const [todos, setTodoList] = useState([]); useEffect(function callApi() { //1. Imagine that we are calling an api to get todos fetchTodosFromApi().then(list => setTodoList(list)); }, []); //2. And we have a state for 2 counter const [ongoingCount, setOngoingCount] = useState(0); const [doneCount, setDoneCount] = useState(0); // 3. We will update the counters when the list changes useEffect(function refreshCounters() { setDoneCount(todos.filter(t => t.done).length); setOngoingCount(todos.filter(t => !t.done).length); }, [todos]); |
Code complet dispo (avec commentaires et explications des soucis) sur : https://github.com/Nikoms/clean-todo/commit/5be102ae9e2eed1a4affd9586d71c8b972c0df4d
Le problème avec cela, c’est qu’on a beaucoup trop de « render ». Sans aucune interaction, on en a déjà 3:
- Avant le fetchTodosFromApi
- Après le fetchTodosFromApi
- Après avoir mis à jour les compteurs via refreshCounters
Pas de state -> pas de useEffect -> pas de re-render.
Et pourquoi pas se passer des state pour les deux compteurs?
1 2 3 |
//2. One solution would be to recalculate the counters on each render const ongoingCount = todos.filter(t => t.done).length; const doneCount = todos.filter(t => !t.done).length; |
Code complet: https://github.com/Nikoms/clean-todo/commit/1a36045110ef98db5f2d8ab2ff43af0a5808c397
C’est bien, mais là, on va recalculer les compteurs à chaque render. C’est pas trop grave pour certaines parties de code, mais dans notre cas, partons dans l’idée que c’est un « gros process ».
useMemo si besoin…
Si les performances s’en font ressentir, on peut très bien utiliser useMemo:
1 2 |
const ongoingCount = useMemo(() => todos.filter(t => t.done).length, [todos]); const doneCount = useMemo(() => todos.filter(t => !t.done).length, [todos]); |
Code complet: https://github.com/Nikoms/clean-todo/commit/09b942f07122323e7229b8bbf32064d7e851be12
Ca fait le même job que le useEffect au début, sauf que le calcul ne nécessite pas un re-render.
Bonus point: Async useEffect
Un truc intéressant à savoir pour les useEffect qui exécute des setState dans une méthode async:
1 2 3 4 5 6 7 8 9 |
useEffect(function refreshCounters() { (async () => { new Promise(resolve => setTimeout(() => { setDoneCount(todos.filter(t => t.done).length); setOngoingCount(todos.filter(t => !t.done).length); resolve(); }, 1)); })(); }, [todos]); |
Code complet: https://github.com/Nikoms/clean-todo/commit/3a952053adcb4b9e12aa8140c6550daa5cf2dd24
Bien sûr que personne ne va écrire ce genre de code (la promise n’a aucun interêt), mais pour le fun: Savez vous combien de render il va y avoir? Au moment où mes todos changent, l’exécution du useEffect se fera à la vitesse de la lumière (car il n’y a rien à faire, si ce n’est lancer une Promise qui sera finie plus tard…).
Ensuite, il y aura autant de re-render qu’il y a de setState. Dans ce cas-ci, il y aura donc 2 render…
Dans un useEffect « classique » (non-async) par contre, il n’y aurait eu qu’un seul re-render, car React ne se re-render qu’à la fin du useEffect… Bon à savoir 🙂