Dans cet article, je vais vous montrer comment utiliser React pour remplacer useEffect dans la plupart des cas.
J'ai regardé "Goodbye, useEffect" de David Khoursid , et c'est 🤯 époustouflant d'une 😀 bonne manière. Je suis d'accord que useEffect a été tellement utilisé qu'il rend notre code sale et difficile à maintenir. J'utilise useEffect depuis longtemps et je suis coupable d'en avoir fait un mauvais usage. Je suis sûr que React a des fonctionnalités qui rendront mon code plus propre et plus facile à entretenir.
useEffect est un crochet qui nous permet d'effectuer des effets secondaires dans les composants fonctionnels. Il combine componentDidMount, componentDidUpdate et componentWillUnmount dans une seule API. C'est un crochet convaincant qui nous permettra de faire beaucoup de choses. Mais c'est aussi un crochet très dangereux qui peut causer beaucoup de bugs.
Jetons un œil à 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> }
C'est un simple compteur qui augmente chaque seconde. Il utilise useEffect pour définir un intervalle. Il utilise également useEffect pour effacer l'intervalle lorsque le composant se démonte. L'extrait de code ci-dessus est un cas d'utilisation répandu pour useEffect.
C'est un exemple simple, mais c'est aussi un exemple terrible.
Le problème avec cet exemple est que l'intervalle est défini à chaque fois que le composant est restitué. Si le composant est restitué pour une raison quelconque, l'intervalle sera à nouveau défini. L'intervalle sera appelé deux fois par seconde. Ce n'est pas un problème avec cet exemple simple, mais cela peut être un gros problème lorsque l'intervalle est plus complexe. Cela peut également provoquer des fuites de mémoire.
Comment le réparer?
Il existe de nombreuses façons de résoudre ce problème. Une façon consiste à 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 bien meilleur que l'exemple précédent. Il ne définit pas l'intervalle à chaque fois que le composant est restitué. Mais il a encore besoin d'être amélioré. C'est encore un peu compliqué. Et il utilise toujours useEffect, qui est un crochet très dangereux.
Comme nous le savons sur useEffect, il combine componentDidMount, componentDidUpdate et componentWillUnmount dans une seule API. Donnons-en quelques exemples :
useEffect(() => { // componentDidMount? }, [])
useEffect(() => { // componentDidUpdate? }, [something, anotherThing])
useEffect(() => { return () => { // componentWillUnmount? } }, [])
C'est facile à comprendre. useEffect est utilisé pour effectuer des effets secondaires lorsque le composant se monte, se met à jour et se démonte. Mais ce n'est pas seulement utilisé pour effectuer des effets secondaires. Il est également utilisé pour effectuer des effets secondaires lorsque le composant est restitué. Ce n'est pas une bonne idée d'effectuer des effets secondaires lorsque le composant est restitué. Cela peut causer beaucoup de bugs. Il est préférable d'utiliser d'autres crochets pour effectuer des effets secondaires lorsque le composant est restitué.
useEffect n'est pas un hook de cycle de vie.
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>Number of changes: {count}</div> </div> ) }
useEffect n'est pas un setter d'état
import React, { useState, useEffect } from 'react' const Example = () => { const [count, setCount] = useState(0) // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times` }) // <-- this is the problem, 😱 it's missing the dependency array return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ) }
Je recommande de lire cette documentation :https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Impératif : Lorsqu'il se passe quelque chose, exécutez cet effet.
Déclaratif : lorsque quelque chose se produit, l'état change et, en fonction (tableau de dépendances) des parties de l'état modifiées, cet effet doit être exécuté, mais uniquement si une condition est vraie. Et React peut l'exécuter à nouveau sans raison de rendu simultané.
Concept :
useEffect(() => { doSomething() return () => cleanup() }, [whenThisChanges])
Mise en œuvre :
useEffect(() => { if (foo && bar && (baz || quo)) { doSomething() } else { doSomethingElse() } // oops, I forgot the cleanup }, [foo, bar, baz, quo])
Implémentation dans le monde réel :
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 foiré. 😱🤮
React 18 exécute les effets deux fois sur la monture (en mode strict). Monter/effet (╯°□°)╯︵ ┻━┻ -> Démonter (simulé)/nettoyer ┬─┬ /( º _ º /) -> Remonter/effet (╯°□°)╯︵ ┻━┻
Doit-il être placé à l'extérieur du composant ? L'effet useEffect par défaut ? Euh... gênant. Hmm... 🤔 Nous ne pouvions pas le mettre dans le rendu car il ne devrait pas y avoir d'effets secondaires car le rendu est comme le côté droit d'une équation mathématique. Ce ne devrait être que le résultat du calcul.
Synchronisation
useEffect(() => { const sub = createThing(input).subscribe((value) => { // do something with value }) 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) } }, [])
Fire-and-forget Synchronized (Action effects) (Activity effects) 0 ---------------------- ----------------- - - - oo | A | oo | A | A oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo V | V oo V | V | o--------------------------------------------------------------------------------> Unmount Remount
Gestionnaires d'événements. En quelque sorte.
<form onSubmit={(event) => { // 💥 side-effect! submitData(event) }} > {/* ... */} </form>
Il y a d'excellentes informations dans Beta React.js. Je recommande de le lire. En particulier le "Les gestionnaires d'événements peuvent-ils avoir des effets secondaires ?" partie .
Absolument! <u>Les gestionnaires d'événements sont le meilleur endroit pour les effets secondaires.</u>
Une autre excellente ressource que je veux mentionner est Où vous pouvez causer des effets secondaires
Dans React, <u>les effets secondaires appartiennent généralement aux gestionnaires d'événements.</u>
Si vous avez épuisé toutes les autres options et que vous ne trouvez pas le bon gestionnaire d'événements pour votre effet secondaire, vous pouvez toujours l'attacher à votre JSX renvoyé avec un appel <u>useEffect</u> dans votre composant. Cela indique à React de l'exécuter plus tard, après le rendu, lorsque les effets secondaires sont autorisés. <u> Cependant, cette approche devrait être votre dernier recours. </u>
"Les effets se produisent en dehors du rendu" - David Khoursid.
(state) => UI (state, event) => nextState // 🤔 Effects?
L'assurance-chômage est une fonction de l'état. Comme tous les états actuels sont rendus, cela produira l'interface utilisateur actuelle. De même, lorsqu'un événement se produit, il crée un nouvel état. Et lorsque l'état change, il crée une nouvelle interface utilisateur. Ce paradigme est au cœur de React.
Intergiciel ? 🕵️ Rappels ? 🤙 Des sagas ? 🧙♂️ Des réactions ? 🧪 Des éviers ? 🚰 Monades (?) 🧙♂️ Quand? 🤷♂️
Transitions d'état. Toujours.
(state, event) => nextState | V (state, event) => (nextState, effect) // Here
Où vont les effets d'action ? Gestionnaires d'événements. Transitions d'état.
Qui s'exécutent en même temps que les gestionnaires d'événements.
Nous pourrions utiliser useEffect car nous ne savons pas qu'il existe déjà une API intégrée de React qui puisse résoudre ce problème.
Voici une excellente ressource à lire sur ce sujet : Vous n'avez peut-être pas besoin d'un effet
useEffect ➡️ useMemo (même si nous n'avons 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-le et repensez-y 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, nous pouvons utiliser useMemo
pour mémoriser le total. Même si la variable n'est pas un calcul coûteux, nous n'avons pas besoin d'utiliser useMemo
pour la mémoriser car nous échangeons essentiellement des performances contre de la mémoire.
Chaque fois que nous voyons setState
dans useEffect
, c'est un signe d'avertissement que nous pouvons le simplifier.
useEffect ➡️ useSyncExternalStore
❌ Mauvais chemin :
const Store = () => { const [isConnected, setIsConnected] = useState(true) useEffect(() => { const sub = storeApi.subscribe(({ status }) => { setIsConnected(status === 'connected') }) return () => { sub.unsubscribe() } }, []) // ... }
✅ Meilleur moyen :
const Store = () => { const isConnected = useSyncExternalStore( // 👇 subscribe storeApi.subscribe, // 👇 get snapshot () => storeApi.getStatus() === 'connected', // 👇 get server snapshot true ) // ... }
useEffect ➡️ eventHandler
❌ Mauvais chemin :
const ChildProduct = ({ onOpen, onClose }) => { const [isOpen, setIsOpen] = useState(false) useEffect(() => { if (isOpen) { onOpen() } else { onClose() } }, [isOpen]) return ( <div> <button onClick={() => { setIsOpen(!isOpen) }} > Toggle quick view </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={} > Toggle quick view </button> </div> ) }
✅ Le meilleur moyen est de créer un crochet 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} > Toggle quick view </button> </div> ) }
useEffect ➡️ justCallIt
❌ Mauvais chemin :
const Store = () => { useEffect(() => { storeApi.authenticate() // 👈 This will run twice! }, []) // ... }
🔨 Réparons-le :
const Store = () => { const didAuthenticateRef = useRef() useEffect(() => { if (didAuthenticateRef.current) return storeApi.authenticate() didAuthenticateRef.current = true }, []) // ... }
➿ Une autre manière :
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 = () => { // ... }
🧪 Tester ?
const renderApp = () => { if (typeof window !== 'undefined') { storeApi.authenticate() } appRoot.render(<Store />) }
Nous n'avons pas nécessairement besoin de tout placer à l'intérieur d'un composant.
useEffect ➡️ renderAsYouFetch (SSR) ou useSWR (CSR)
❌ Mauvais chemin :
const Store = () => { const [items, setItems] = useState([]) useEffect(() => { let isCanceled = false getItems().then((data) => { if (isCanceled) return setItems(data) }) return () => { isCanceled = true } }) // ... }
💽 Façon 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/wait en mode Server Component :
// app/page.tsx async function getData() { const res = await fetch('https://api.example.com/...') // The return value is *not* serialized // You can return Date, Map, Set, etc. // Recommendation: handle errors if (!res.ok) { // This will activate the closest `error.js` Error Boundary throw new Error('Failed to fetch data') } 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>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {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>hello {data}!</div> }
⏭️💁 Next.js (pagesDir) en mode RSE :
// pages/index.tsx import useSWR from 'swr' export default function Page() { const { data, error } = useSWR('/api/data', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data}!</div> }
🍃 React Query (façon SSR :
import { getItems } from './storeApi' import { useQuery } from 'react-query' const Store = () => { const queryClient = useQueryClient() return ( <button onClick={() => { queryClient.prefetchQuery('items', getItems) }} > See items </button> ) } const Items = () => { const { data, isLoading, isError } = useQuery('items', getItems) // ... }
⁉️ Vraiment ⁉️
Que devrions-nous utiliser ? utiliserEffet ? useQuery ? utiliserSWR ?
ou... utilisez simplement() 🤔
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 la RFC React.
function Note({ id }) { // This fetches a note asynchronously, but to the component author, it looks // like a synchronous operation. const note = use(fetchNote(id)) return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> </div> ) }
🏃♂️ Conditions de course
🔙 Pas de bouton de retour instantané
🔍 Pas de SSR ou de contenu HTML initial
🌊 Chasser la cascade
- Reddit, Dan Abramov
De la récupération de données à la lutte contre les API impératives, les effets secondaires sont l'une des sources de frustration les plus importantes dans le développement d'applications Web. Et soyons honnêtes, tout mettre en œuvre Les crochets d'effet n'aident que peu. Heureusement, il existe une science (enfin, les mathématiques) des effets secondaires, formalisée dans des machines à états et des diagrammes d'états, qui peut nous aider à modéliser visuellement et à comprendre comment orchestrer les effets, quelle que soit leur complexité déclarative.