- Publicado el
Hola React, Adiós useEffect (Espero)
- Autores
- Nombre
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
En este artículo, te mostraré cómo usar React para reemplazar useEffect en la mayoría de los casos.
He estado viendo "Goodbye, useEffect" de David Khoursid, y es 🤯 me vuela la cabeza de una 😀 buena manera. Estoy de acuerdo en que useEffect se ha utilizado tanto que hace que nuestro código sea sucio y difícil de mantener. He estado usando useEffect durante mucho tiempo y soy culpable de usarlo mal. 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 useEffect?
useEffect es un hook que nos permite realizar efectos secundarios en componentes funcionales. Combina componentDidMount, componentDidUpdate y componentWillUnmount en una sola API. Es un hook convincente que nos permitirá hacer muchas cosas. Pero también es un hook 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 utiliza useEffect para borrar el intervalo cuando el componente se desmonta. El fragmento de código anterior es un caso de uso generalizado de useEffect. Es un ejemplo sencillo, pero también un ejemplo terrible.
El problema con este ejemplo es que el intervalo se establece cada vez que el componente vuelve a renderizarse. Si el componente se vuelve a renderizar por cualquier motivo, el intervalo se volverá a establecer. El intervalo se llamará dos veces por segundo. No es un problema con este simple ejemplo, pero puede ser un gran problema cuando el intervalo es más complejo. También puede causar fugas de memoria.
¿Cómo arreglarlo?
Hay muchas maneras 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 mejoras. Sigue siendo un poco complicado. Y todavía usa useEffect, que es un hook muy peligroso.
useEffect no es para efectos
Como sabemos sobre useEffect, combina componentDidMount, componentDidUpdate y componentWillUnmount en una sola API. Démos 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 utiliza para realizar efectos secundarios. También se utiliza para realizar efectos secundarios cuando el componente vuelve a renderizarse. No es una buena idea realizar efectos secundarios cuando el componente se vuelve a renderizar. Puede causar muchos errores. Es mejor usar otros hooks para realizar efectos secundarios cuando el componente se vuelve a renderizar.
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>Número de cambios: {count}</div>
</div>
)
}
useEffect no es un setter de estado
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Similar a componentDidMount y componentDidUpdate:
useEffect(() => {
// Actualiza el título del documento usando la API del navegador
document.title = `Has hecho clic ${count} veces`
}) // <-- este es el problema, 😱 falta el array de dependencias
return (
<div>
<p>Has hecho clic {count} veces</p>
<button onClick={() => setCount(count + 1)}>Haz clic en mí</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 sucede, ejecuta este efecto.
Declarativo: Cuando algo sucede, provocará un cambio en el estado y, dependiendo (array de dependencias) de qué partes del estado hayan cambiado, este efecto debería ejecutarse, pero solo si se cumple alguna condición. Y React puede ejecutarlo de nuevo por ninguna razón renderizado concurrente.
Concepto vs Implementación
Concepto:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Implementación:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// ups, me olvidé de la limpieza
}, [foo, bar, baz, quo])
Implementación del 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. 😱🤮
¿Dónde van los efectos?
React 18 ejecuta efectos dos veces en el montaje (en modo estricto). Monte/efecto (╯°□°)╯︵ ┻━┻ -> Desmonte (simulado)/limpieza ┬─┬ /( º _ º /) -> Vuelva a montar/efecto (╯°□°)╯︵ ┻━┻
¿Debería colocarse fuera del componente? ¿El useEffect predeterminado? Uh... incómodo. Hmm... 🤔 No pudimos ponerlo en render ya que no debería haber efectos secundarios allí porque render es como el lado derecho de una ecuación matemática. Debería ser solo el resultado del cálculo.
¿Para qué es useEffect?
Sincronización
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// haz algo con 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 vs efectos de actividad
Dispara y olvida Sincronizado
(Efectos de acción) (Efectos de actividad)
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-------------------------------------------------------------------------------->
Desmontar Volver a montar
¿Dónde van los efectos de acción?
Controladores de eventos. Más o menos.
<form
onSubmit={(event) => {
// 💥 ¡efecto secundario!
submitData(event)
}}
>
{/* ... */}
</form>
Hay excelente información en Beta React.js. Recomiendo leerlo. Especialmente la parte "¿Pueden los controladores de eventos tener efectos secundarios?".
¡Absolutamente! Los controladores de eventos son el mejor lugar para los efectos secundarios.
Otro gran recurso que quiero mencionar es Dónde puedes causar efectos secundarios
En React, los efectos secundarios generalmente pertenecen dentro de los controladores de eventos.
Si has agotado todas las demás opciones y no puedes encontrar el controlador de eventos adecuado para tu efecto secundario, aún puedes adjuntarlo al JSX devuelto con una llamada useEffect en tu componente. Esto le dice a React que lo ejecute más tarde, después del renderizado, cuando se permitan los efectos secundarios. Sin embargo, este enfoque debe ser tu último recurso.
"Los efectos suceden fuera del renderizado" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 ¿Efectos?
UI es una función del estado. Como todos los estados actuales se renderizan, producirá la UI actual. Del mismo modo, cuando ocurre un evento, creará un nuevo estado. Y cuando el estado cambia, construirá una nueva UI. Este paradigma es el núcleo de React.
¿Cuándo suceden los efectos?
¿Middleware? 🕵️ ¿Callbacks? 🤙 ¿Sagas? 🧙♂️ ¿Reacciones? 🧪 ¿Sinks? 🚰 ¿Monads(?) 🧙♂️ ¿Cuándo sea? 🤷♂️
Transiciones de estado. Siempre.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Aquí
![Ilustración de re-renderizado](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
¿Dónde van los efectos de acción? Controladores de eventos. Transiciones de estado.
Que resultan ejecutarse al mismo tiempo que los controladores de eventos.
Es posible 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 necesites 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 piensa en ello de nuevo cuidadosamente 🧐.
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.
useSyncExternalStore
Efectos con tiendas externas?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(
// 👇 suscribirse
storeApi.subscribe,
// 👇 obtener instantánea
() => storeApi.getStatus() === 'connected',
// 👇 obtener instantánea del servidor
true
)
// ...
}
No necesitamos useEffect para comunicarnos con los padres.
useEffect ➡️ controlador de eventos
❌ Manera incorrecta:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Alternar vista rápida
</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={}
>
Alternar vista rápida
</button>
</div>
)
}
✅ La mejor manera es crear un hook 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}
>
Alternar vista rápida
</button>
</div>
)
}
No necesitamos useEffect para inicializar singletons globales.
useEffect ➡️ justCallIt
❌ Manera incorrecta:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 ¡Esto se ejecutará dos veces!
}, [])
// ...
}
🔨 Arreglemos esto:
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
}, [])
// ...
}
🤔 ¿Qué pasa 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 es necesario 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
}
})
// ...
}
💽 Manera de 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) con async/await en Server Component way:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// El valor devuelto *no* está serializado
// Puedes devolver Date, Map, Set, etc.
// Recomendación: manejar errores
if (!res.ok) {
// Esto activará el `error.js` Error Boundary más cercano
throw new Error('Error al obtener datos')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) con useSWR en Client Component way:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>error al cargar</div>
if (!data) return <div>cargando...</div>
return <div>hola {data}!</div>
}
⏭️🧹 Next.js (pagesDir) en SSR way:
// 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>hola {data}!</div>
}
⏭️💁 Next.js (pagesDir) en CSR way:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>error al cargar</div>
if (!data) return <div>cargando...</div>
return <div>hola {data}!</div>
}
🍃 React Query (SSR way:
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
Ver artículos
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Realmente ⁉️ ¿Qué deberíamos usar? ¿useEffect? ¿useQuery? ¿useSWR?
o... solo use() 🤔
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 los componentes, hooks y Suspense. Obtén más información sobre use() en el RFC de React.
function Note({ id }) {
// Esto obtiene una nota de forma asincrónica, pero para el autor del componente, parece
// una operación sincrónica.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Problemas al obtener datos en useEffect
🏃♂️ Condiciones de carrera
🔙 No hay botón de retroceso instantáneo
🔍 No hay contenido SSR o HTML inicial
🌊 Perseguir una cascada
- Reddit, Dan Abramov
Conclusión
Desde la obtención de datos hasta la lucha 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 hooks useEffect solo ayuda un poco. Afortunadamente, existe una ciencia (bueno, matemáticas) para los efectos secundarios, formalizada en máquinas de estado y diagramas de estado, que puede ayudarnos a modelar y comprender visualmente cómo orquestar los efectos, sin importar cuán complejos sean declarativamente.