paint-brush
Cómo usar React para reemplazar useEffectpor@imamdev
3,449 lecturas
3,449 lecturas

Cómo usar React para reemplazar useEffect

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

Demasiado Largo; Para Leer

useEffect es un gancho que nos permite realizar efectos secundarios en componentes de funciones. Combina componenteDidMount, componenteDidUpdate y componenteWillUnmount en una sola API. Es un gancho convincente que nos permitirá hacer muchas cosas. Pero también es un gancho muy peligroso que puede causar muchos errores. En este artículo, le mostraré cómo usar React para reemplazar useEffect en la mayoría de los casos.
featured image - Cómo usar React para reemplazar useEffect
Imamuzzaki Abu Salam HackerNoon profile picture

En este artículo, le mostraré cómo usar React para reemplazar useEffect en la mayoría de los casos.


He estado viendo "Goodbye, useEffect" de David Khoursid , y me 🤯 me sorprende de una 😀 buena manera. Estoy de acuerdo en que useEffect se ha usado tanto que ensucia nuestro código y es difícil de mantener. He estado usando useEffect durante mucho tiempo y soy culpable de mal uso. Estoy seguro de que React tiene características que harán que mi código sea más limpio y fácil de mantener.

¿Qué es el efecto de uso?

useEffect es un gancho que nos permite realizar efectos secundarios en componentes de función. Combina componenteDidMount, componenteDidUpdate y componenteWillUnmount en una sola API. Es un gancho convincente que nos permitirá hacer muchas cosas. Pero también es un gancho muy peligroso que puede causar muchos errores.

¿Por qué useEffect es peligroso?

Echemos un vistazo al siguiente ejemplo:

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

Es un contador simple que aumenta cada segundo. Utiliza useEffect para establecer un intervalo. También usa useEffect para borrar el intervalo cuando el componente se desmonta. El fragmento de código anterior es un caso de uso generalizado para useEffect.


Es un ejemplo sencillo, pero también es un ejemplo terrible.


El problema con este ejemplo es que el intervalo se establece cada vez que el componente se vuelve a renderizar. Si el componente se vuelve a renderizar por algún motivo, el intervalo se establecerá de nuevo. El intervalo se llamará dos veces por segundo. No es un problema con este ejemplo simple, pero puede ser un gran problema cuando el intervalo es más complejo. También puede causar pérdidas de memoria.


¿Como arreglarlo?

Hay muchas formas de solucionar este problema. Una forma es usar useRef para almacenar el intervalo.

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

El código anterior es mucho mejor que el ejemplo anterior. No establece el intervalo cada vez que el componente se vuelve a renderizar. Pero todavía necesita mejorar. Todavía es un poco complicado. Y todavía usa useEffect, que es un gancho muy peligroso.

useEffect no es para efectos

Como sabemos, useEffect combina componenteDidMount, componenteDidUpdate y componenteWillUnmount en una sola API. Pongamos algunos ejemplos de ello:

 useEffect(() => { // componentDidMount? }, [])
 useEffect(() => { // componentDidUpdate? }, [something, anotherThing])
 useEffect(() => { return () => { // componentWillUnmount? } }, [])

Es fácil de entender. useEffect se utiliza para realizar efectos secundarios cuando el componente se monta, actualiza y desmonta. Pero no solo se usa para realizar efectos secundarios. También se usa para realizar efectos secundarios cuando el componente se vuelve a renderizar. No es una buena idea realizar efectos secundarios cuando el componente se vuelve a renderizar. Puede causar muchos errores. Es mejor usar otros ganchos para realizar efectos secundarios cuando el componente se vuelve a renderizar.


useEffect no es un gancho de ciclo de vida.


 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 no es un establecedor de estado


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

Recomiendo leer esta documentación:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects


Imperativo vs Declarativo

Imperativo : Cuando algo suceda, ejecuta este efecto.

Declarativo : cuando algo sucede, hará que el estado cambie y dependiendo (matriz de dependencia) de qué partes del estado cambiaron, este efecto debe ejecutarse, pero solo si alguna condición es verdadera. Y React puede ejecutarlo nuevamente sin ningún motivo de representación simultánea.


Concepto vs Implementación

Concepto :

 useEffect(() => { doSomething() return () => cleanup() }, [whenThisChanges])

Implementación :

 useEffect(() => { if (foo && bar && (baz || quo)) { doSomething() } else { doSomethingElse() } // oops, I forgot the cleanup }, [foo, bar, baz, quo])

Implementación en el mundo real :

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

Da miedo escribir este código. Además, será normal en nuestra base de código y estará desordenado. 😱🤮

¿Adónde van los efectos?

React 18 ejecuta los efectos dos veces en la montura (en modo estricto). Montaje/efecto (╯°□°)╯︵ ┻━┻ -> Desmontaje (simulado)/limpieza ┬─┬ /( º _ º /) -> Remontaje/efecto (╯°□°)╯︵ ┻━┻

¿Debe colocarse fuera del componente? ¿El efecto de uso predeterminado? Eh... incómodo. Hmm... 🤔 No pudimos ponerlo en render porque no debería haber efectos secundarios porque el render es como la mano derecha de una ecuación matemática. Debe ser sólo el resultado del cálculo.

¿Para qué sirve useEffect?

Sincronización

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

Efectos de acción frente a efectos de actividad

 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

¿Adónde van los efectos de acción?

Manejadores de eventos. más o menos

 <form onSubmit={(event) => { // 💥 side-effect! submitData(event) }} > {/* ... */} </form>

Hay excelente información en Beta React.js. Recomiendo leerlo. Especialmente el "¿Pueden los controladores de eventos tener efectos secundarios?" parte


¡Absolutamente! <u>Los controladores de eventos son el mejor lugar para los efectos secundarios.</u>


Otro gran recurso que quiero mencionar es Dónde puede causar efectos secundarios


En React, <u>los efectos secundarios generalmente pertenecen a los controladores de eventos.</u>

Si ha agotado todas las demás opciones y no puede encontrar el controlador de eventos correcto para su efecto secundario, aún puede adjuntarlo a su JSX devuelto con una llamada <u>useEffect</u> en su componente. Esto le dice a React que lo ejecute más tarde, después de renderizar, cuando se permitan los efectos secundarios. <u> Sin embargo, este enfoque debería ser su último recurso. </u>


"Los efectos ocurren fuera del renderizado" - David Khoursid.

 (state) => UI (state, event) => nextState // 🤔 Effects?


UI es una función del estado. A medida que se representen todos los estados actuales, se producirá la interfaz de usuario actual. Asimismo, cuando sucede un evento, creará un nuevo estado. Y cuando el estado cambie, creará una nueva interfaz de usuario. Este paradigma es el núcleo de React.

¿Cuándo ocurren los efectos?

¿Middleware? 🕵️ ¿Devoluciones de llamadas? 🤙 Sagas? 🧙‍♂️ ¿Reacciones? 🧪 ¿Fregaderos? 🚰 Mónadas(?) 🧙‍♂️ ¿Cuándo? 🤷‍♂️

Transiciones de estado. Siempre.

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

Reproducir imagen de ilustración

¿Adónde van los efectos de acción? Manejadores de eventos. Transiciones de estado.

Que se ejecutan al mismo tiempo que los controladores de eventos.

Puede que no necesitemos efectos

Podríamos usar useEffect porque no sabemos que ya existe una API integrada de React que puede resolver este problema.


Aquí hay un excelente recurso para leer sobre este tema: Es posible que no necesite un efecto

No necesitamos useEffect para transformar datos.

useEffect ➡️ useMemo (aunque no necesitamos useMemo en la mayoría de los casos)

 const Cart = () => { const [items, setItems] = useState([]) const [total, setTotal] = useState(0) useEffect(() => { setTotal(items.reduce((total, item) => total + item.price, 0)) }, [items]) // ... }

Lee y vuelve a pensarlo detenidamente 🧐.

 const Cart = () => { const [items, setItems] = useState([]) const total = useMemo(() => { return items.reduce((total, item) => total + item.price, 0) }, [items]) // ... }

En lugar de usar useEffect para calcular el total, podemos usar useMemo para memorizar el total. Incluso si la variable no es un cálculo costoso, no necesitamos usar useMemo para memorizarla porque básicamente estamos intercambiando rendimiento por memoria.


Cada vez que vemos setState en useEffect , es una señal de advertencia de que podemos simplificarlo.

¿Efectos con tiendas externas? usarSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ Manera incorrecta:

 const Store = () => { const [isConnected, setIsConnected] = useState(true) useEffect(() => { const sub = storeApi.subscribe(({ status }) => { setIsConnected(status === 'connected') }) return () => { sub.unsubscribe() } }, []) // ... }

✅ Mejor manera:

 const Store = () => { const isConnected = useSyncExternalStore( // 👇 subscribe storeApi.subscribe, // 👇 get snapshot () => storeApi.getStatus() === 'connected', // 👇 get server snapshot true ) // ... }

No necesitamos useEffect para comunicarnos con los padres.

useEffect ➡️ eventHandler

❌ Manera incorrecta:

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

📈 Mejor manera:

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

✅ La mejor manera es crear un gancho personalizado:

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

No necesitamos useEft para inicializar singletons globales.

useEffect ➡️ solo llámalo

❌ Manera incorrecta:

 const Store = () => { useEffect(() => { storeApi.authenticate() // 👈 This will run twice! }, []) // ... }

🔨 Arreglemoslo:

 const Store = () => { const didAuthenticateRef = useRef() useEffect(() => { if (didAuthenticateRef.current) return storeApi.authenticate() didAuthenticateRef.current = true }, []) // ... }

➿ Otra forma:

 let didAuthenticate = false const Store = () => { useEffect(() => { if (didAuthenticate) return storeApi.authenticate() didAuthenticate = true }, []) // ... }

🤔 Cómo si:

 storeApi.authenticate() const Store = () => { // ... }

🍷 SSR, ¿eh?

 if (typeof window !== 'undefined') { storeApi.authenticate() } const Store = () => { // ... }

🧪 ¿Pruebas?

 const renderApp = () => { if (typeof window !== 'undefined') { storeApi.authenticate() } appRoot.render(<Store />) }

No necesariamente necesitamos colocar todo dentro de un componente.

No necesitamos useEffect para obtener datos.

useEffect ➡️ renderAsYouFetch (SSR) o useSWR (CSR)

❌ Manera incorrecta:

 const Store = () => { const [items, setItems] = useState([]) useEffect(() => { let isCanceled = false getItems().then((data) => { if (isCanceled) return setItems(data) }) return () => { isCanceled = true } }) // ... }

💽 Forma de remezcla:

 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) con async/await en forma de componente de servidor:

 // 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) con useSWR en forma de componente de cliente:

 // 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 modo 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 modo CSR:

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

🍃 Consulta de reacción (forma 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) // ... }

⁉️ De verdad ⁉️


¿Qué debemos usar? usarEfecto? usarQuery? ¿Usar ROE?

o... simplemente usa() 🤔


use() es una nueva función de React que acepta una promesa conceptualmente similar a await. use() maneja la promesa devuelta por una función de una manera que es compatible con componentes, ganchos y suspenso. Obtenga más información sobre use() en React RFC.

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

Recuperación en usoProblemas de efectos

🏃‍♂️ Condiciones de carrera

🔙 Sin botón de retroceso instantáneo

🔍 Sin SSR ni contenido HTML inicial

🌊 Persiguiendo cascada

  • Reddit, Dan Abramov

Conclusión

Desde obtener datos hasta luchar con API imperativas, los efectos secundarios son una de las fuentes de frustración más importantes en el desarrollo de aplicaciones web. Y seamos honestos, poner todo en uso Los ganchos de efectos solo ayudan un poco. Afortunadamente, existe una ciencia (bueno, matemáticas) para los efectos secundarios, formalizada en máquinas de estado y gráficos de estado, que puede ayudarnos a modelar visualmente y comprender cómo orquestar los efectos, sin importar cuán complejos se vuelvan declarativamente.

Recursos