- Publié le
Adieu React, adieu useEffect (on l'espère)
- Auteurs
- Nom
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Comment remplacer useEffect dans la plupart des cas avec React
Dans cet article, je vais vous montrer comment utiliser React pour remplacer useEffect
dans la plupart des cas.
J'ai regardé la vidéo "Goodbye, useEffect" de David Khoursid, et ça m'a 🤯 époustouflé d'une manière 😀 positive. Je suis d'accord que useEffect
a été utilisé tellement que cela rend notre code sale et difficile à maintenir. J'utilise useEffect
depuis longtemps, et je suis coupable de l'avoir mal utilisé. Je suis sûr que React a des fonctionnalités qui rendront mon code plus propre et plus facile à maintenir.
Qu'est-ce que useEffect ?
useEffect
est un hook qui nous permet d'effectuer des effets secondaires dans les composants fonctionnels. Il combine componentDidMount
, componentDidUpdate
et componentWillUnmount
en une seule API. C'est un hook convaincant qui nous permettra de faire beaucoup de choses. Mais c'est aussi un hook très dangereux qui peut causer beaucoup de bugs.
Pourquoi useEffect est-il dangereux ?
Prenons l'exemple suivant :
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>{count}</div>
}
Il s'agit d'un simple compteur qui augmente toutes les secondes. Il utilise useEffect
pour définir un intervalle. Il utilise également useEffect
pour effacer l'intervalle lorsque le composant est démonté. Le code ci-dessus est un cas d'utilisation courant de useEffect
. C'est un exemple simple, mais c'est aussi un terrible exemple.
Le problème avec cet exemple est que l'intervalle est défini à chaque fois que le composant est re-rendu. Si le composant est re-rendu pour une raison quelconque, l'intervalle sera défini à nouveau. L'intervalle sera appelé deux fois par seconde. Ce n'est pas un problème avec cet exemple simple, mais cela peut poser un gros problème lorsque l'intervalle est plus complexe. Cela peut également provoquer des fuites de mémoire.
Comment corriger cela ?
Il existe de nombreuses façons de corriger ce problème. Une façon est d'utiliser useRef
pour stocker l'intervalle.
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef()
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>{count}</div>
}
Le code ci-dessus est beaucoup mieux que l'exemple précédent. Il ne définit pas l'intervalle à chaque fois que le composant est re-rendu. Mais il a encore besoin d'améliorations. C'est encore un peu compliqué. Et il utilise toujours useEffect
, qui est un hook très dangereux.
useEffect n'est pas pour les effets
Comme nous le savons à propos de useEffect
, il combine componentDidMount
, componentDidUpdate
et componentWillUnmount
en une seule API. Donnons quelques exemples de cela :
useEffect(() => {
// componentDidMount ?
}, [])
useEffect(() => {
// componentDidUpdate ?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount ?
}
}, [])
C'est facile à comprendre. useEffect
est utilisé pour effectuer des effets secondaires lorsque le composant est monté, mis à jour et démonté. Mais il n'est pas seulement utilisé pour effectuer des effets secondaires. Il est également utilisé pour effectuer des effets secondaires lorsque le composant est re-rendu. Ce n'est pas une bonne idée d'effectuer des effets secondaires lorsque le composant est re-rendu. Cela peut causer beaucoup de bugs. Il est préférable d'utiliser d'autres hooks pour effectuer des effets secondaires lorsque le composant est re-rendu.
import React, { useState, useEffect } from 'react'
const Example = () => {
const [value, setValue] = useState('')
const [count, setCount] = useState(-1)
useEffect(() => {
setCount(count + 1)
})
const onChange = ({ target }) => setValue(target.value)
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>Nombre de changements : {count}</div>
</div>
)
}
useEffect
n'est pas un setter d'état
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Similaire à componentDidMount et componentDidUpdate :
useEffect(() => {
// Mettre à jour le titre du document en utilisant l'API du navigateur
document.title = `Vous avez cliqué ${count} fois`
}) // <-- c'est le problème, 😱 il manque le tableau de dépendances
return (
<div>
<p>Vous avez cliqué {count} fois</p>
<button onClick={() => setCount(count + 1)}>Cliquez sur moi</button>
</div>
)
}
Je recommande de lire cette documentation : https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Impératif vs Déclaratif
Impératif: Lorsque quelque chose se produit, exécutez cet effet.
Déclaratif: Lorsque quelque chose se produit, cela entraînera une modification de l'état et en fonction (tableau de dépendances) des parties de l'état qui ont changé, cet effet doit être exécuté, mais seulement si une certaine condition est vraie. Et React peut l'exécuter à nouveau pour aucune raison rendu concurrent.
Concept vs Implémentation
Concept:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Implémentation:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// Oups, j'ai oublié le nettoyage
}, [foo, bar, baz, quo])
Implémentation réelle:
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false)
}
setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])
C'est effrayant d'écrire ce code. De plus, ce sera normal dans notre base de code et sera un gâchis. 😱🤮
Où vont les effets ?
React 18 exécute les effets deux fois au montage (en mode strict). Montage/effet (╯°□°)╯︵ ┻━┻ -> Démontage (simulé)/nettoyage ┬─┬ /( º _ º /) -> Remontage/effet (╯°□°)╯︵ ┻━┻
Devrait-il être placé en dehors du composant ? Le useEffect
par défaut ? Euh... gênant. Hmm... 🤔 On ne pouvait pas le mettre dans le rendu car il ne devrait pas y avoir d'effets secondaires là-bas car le rendu est comme le membre droit d'une équation mathématique. Il ne devrait s'agir que du résultat du calcul.
A quoi sert useEffect ?
Synchronisation
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// faire quelque chose avec la valeur
})
return sub.unsubscribe
}, [input])
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
}
elRef.current.addEventListener('pointermove', handler)
return () => {
elRef.current.removeEventListener('pointermove', handler)
}
}, [])
Effets d'action vs effets d'activité
Effets de tir et d'oubli Synchronisé
(Effets d'action) (Effets d'activité)
0 ---------------------- ----------------- - - -
o o | A | o o | A | A
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o V | V o o V | V |
o-------------------------------------------------------------------------------->
Démontage Remontage
Où vont les effets d'action ?
Gestionnaires d'événements. En quelque sorte.
<form
onSubmit={(event) => {
// 💥 effet secondaire !
submitData(event)
}}
>
{/* ... */}
</form>
Il existe d'excellentes informations dans Beta React.js. Je recommande de les lire. En particulier la partie "Can event handlers have side effects?".
Absolument ! Les gestionnaires d'événements sont le meilleur endroit pour les effets secondaires.
Une autre excellente ressource que je veux mentionner est Where you can cause side effects
Dans React, les effets secondaires appartiennent généralement à l'intérieur des gestionnaires d'événements.
Si vous avez épuisé toutes les autres options et ne trouvez pas le bon gestionnaire d'événements pour votre effet secondaire, vous pouvez toujours l'attacher à votre JSX renvoyé avec un appel
useEffect
dans votre composant. Cela indique à React de l'exécuter plus tard, après le rendu, lorsque les effets secondaires sont autorisés. Cependant, cette approche doit être votre dernier recours.
"Les effets se produisent en dehors du rendu" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Effets ?
L'UI est une fonction de l'état. Lorsque tous les états actuels sont rendus, il produira l'UI actuelle. De même, lorsqu'un événement se produit, il créera un nouvel état. Et lorsque l'état change, il construira un nouvel UI. Ce paradigme est au cœur de React.
Quand les effets se produisent-ils ?
Transitions d'état. Toujours.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Ici
![Illustration d'un rendu](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Où vont les effets d'action ? Gestionnaires d'événements. Transitions d'état.
Qui se trouvent être exécutées en même temps que les gestionnaires d'événements.
On n'a peut-être pas besoin d'effets
On pourrait utiliser useEffect
parce qu'on ne sait pas qu'il existe déjà une API intégrée de React qui peut résoudre ce problème.
Voici une excellente ressource pour lire à propos de ce sujet : You Might Not Need an Effect
On n'a pas besoin de useEffect pour transformer des données.
useEffect
➡️ useMemo
(même si on n'a pas besoin de useMemo
dans la plupart des cas)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Lisez et réfléchissez à nouveau attentivement 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Au lieu d'utiliser useEffect
pour calculer le total, on peut utiliser useMemo
pour mémoriser le total. Même si la variable n'est pas un calcul coûteux, on n'a pas besoin d'utiliser useMemo
pour la mémoriser car on échange essentiellement des performances pour de la mémoire.
Chaque fois qu'on voit setState
dans useEffect
, c'est un signe avant-coureur qu'on peut le simplifier.
useSyncExternalStore
Effets avec des magasins externes ?useEffect
➡️ useSyncExternalStore
❌ Mauvaise façon :
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ Meilleure façon :
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 s'abonner
storeApi.subscribe,
// 👇 obtenir un instantané
() => storeApi.getStatus() === 'connected',
// 👇 obtenir un instantané du serveur
true
)
// ...
}
On n'a pas besoin de useEffect pour communiquer avec les parents.
useEffect
➡️ gestionnaire d'événements
❌ Mauvaise façon :
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Activer la vue rapide
</button>
</div>
)
}
📈 Meilleure façon :
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return (
<div>
<button
onClick={}
>
Activer la vue rapide
</button>
</div>
)
}
✅ La meilleure façon est de créer un hook personnalisé :
const useToggle({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return [isOpen, handleToggle]
}
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, handleToggle] = useToggle({ onOpen, onClose })
return (
<div>
<button
onClick={handleToggle}
>
Activer la vue rapide
</button>
</div>
)
}
On n'a pas besoin de useEft pour initialiser des singletons globaux.
useEffect
➡️ justCallIt
❌ Mauvaise façon :
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Cela s'exécutera deux fois !
}, [])
// ...
}
🔨 Corrigeons cela :
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ Une autre façon :
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 Comment si :
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR, hein ?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Test ?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
On n'a pas nécessairement besoin de placer tout dans un composant.
On n'a pas besoin de useEffect pour récupérer des données.
useEffect
➡️ renderAsYouFetch
(SSR) ou useSWR
(CSR)
❌ Mauvaise façon :
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 Way Remix :
import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'
export const loader = async () => {
const items = await getItems()
return json(items)
}
const Store = () => {
const items = useLoaderData()
// ...
}
export default Store
⏭️🧹 Next.js (appDir) avec async/await en composant serveur :
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// La valeur de retour n'est *pas* sérialisée
// Vous pouvez renvoyer Date, Map, Set, etc.
// Recommandation : gérer les erreurs
if (!res.ok) {
// Cela activera la frontière d'erreur `error.js` la plus proche
throw new Error('Échec de la récupération des données')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) avec useSWR en composant client :
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>Échec du chargement</div>
if (!data) return <div>chargement...</div>
return <div>bonjour {data}!</div>
}
⏭️🧹 Next.js (pagesDir) en mode SSR :
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('https://api.example.com/...')
const data = await res.json()
return {
props: {
data,
},
}
}
export default function Page({ data }) {
return <div>bonjour {data}!</div>
}
⏭️💁 Next.js (pagesDir) en mode CSR :
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>Échec du chargement</div>
if (!data) return <div>chargement...</div>
return <div>bonjour {data}!</div>
}
🍃 React Query (mode SSR) :
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
Voir les articles
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Vraiment ⁉️ Que devrions-nous utiliser ? useEffect ? useQuery ? useSWR ?
ou... juste use() 🤔
use() est une nouvelle fonction React qui accepte une promesse conceptuellement similaire à await. use() gère la promesse renvoyée par une fonction d'une manière compatible avec les composants, les hooks et Suspense. En savoir plus sur use() dans le RFC React.
function Note({ id }) {
// Cela récupère une note de manière asynchrone, mais pour l'auteur du composant, cela ressemble
// à une opération synchrone.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Problèmes de récupération dans useEffect
🏃♂️ Conditions de course
🔙 Pas de bouton retour instantané
🔍 Pas de SSR ou de contenu HTML initial
🌊 Poursuite de la cascade
- Reddit, Dan Abramov
Conclusion
De la récupération de données à la lutte contre les API impératives, les effets secondaires sont l'une des principales sources de frustration dans le développement d'applications Web. Et soyons honnêtes, mettre tout dans les hooks useEffect
ne sert qu'à peu de choses. Heureusement, il existe une science (en fait, des mathématiques) des effets secondaires, formalisée dans les machines à états et les automates à états, qui peut nous aider à modéliser et à comprendre visuellement comment orchestrer les effets, quelle que soit leur complexité, de manière déclarative.