paint-brush
Comment utiliser React pour remplacer useEffectpar@imamdev
3,420 lectures
3,420 lectures

Comment utiliser React pour remplacer useEffect

par Imamuzzaki Abu Salam16m2022/12/31
Read on Terminal Reader

Trop long; Pour lire

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. Dans cet article, je vais vous montrer comment utiliser React pour remplacer useEffect dans la plupart des cas.
featured image - Comment utiliser React pour remplacer useEffect
Imamuzzaki Abu Salam HackerNoon profile picture

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.

Qu'est-ce que useEffect ?

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.

Pourquoi useEffect est dangereux ?

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.

useEffect n'est pas pour les effets

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 vs déclaratif

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 vs mise en œuvre

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é. 😱🤮

Où vont les effets ?

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.

A quoi sert useEffect ?

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) } }, [])

Effets d'action vs effets d'activité

 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

Où vont les effets d'action ?

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.

Quand les effets se produisent-ils ?

Intergiciel ? 🕵️ Rappels ? 🤙 Des sagas ? 🧙‍♂️ Des réactions ? 🧪 Des éviers ? 🚰 Monades (?) 🧙‍♂️ Quand? 🤷‍♂️

Transitions d'état. Toujours.

 (state, event) => nextState | V (state, event) => (nextState, effect) // Here 

Rendre l'image d'illustration

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 n'avons peut-être pas besoin d'effets

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

Nous n'avons pas besoin de useEffect pour transformer les données.

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.

Effets avec les magasins externes ? useSyncExternalStore

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 ) // ... }

Nous n'avons pas besoin de useEffect pour communiquer avec les parents.

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> ) }

Nous n'avons pas besoin de useEft pour initialiser les singletons globaux.

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.

Nous n'avons pas besoin de useEffect pour récupérer des données.

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> ) }

Récupération en cours d'utilisationProblèmes d'effet

🏃‍♂️ Conditions de course

🔙 Pas de bouton de retour instantané

🔍 Pas de SSR ou de contenu HTML initial

🌊 Chasser 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 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.

Ressources