Cuándo usar useEffect
Ya conoces cómo funciona useEffect y su array de dependencias. Pero ahora viene la pregunta más importante: ¿cuándo deberías usarlo realmente?
En esta clase aprenderás los casos de uso prácticos de useEffect, cuándo es necesario y cuándo no, cómo gestionar suscripciones a eventos y por qué la función de limpieza es esencial para evitar memory leaks.
¿Cuándo NO necesitas useEffect?
Antes de aprender cuándo usar useEffect, es importante entender cuándo NO lo necesitas.
❌ No lo uses para transformar datos
Si solo necesitas transformar datos basados en props o estado, no necesitas useEffect. Hazlo directamente en el cuerpo del componente:
// ❌ MAL: Usar useEffect para transformar datos
function ProductList({ products }) {
const [expensiveProducts, setExpensiveProducts] = useState([])
useEffect(() => {
const filtered = products.filter((product) => product.price > 100)
setExpensiveProducts(filtered)
}, [products])
return <div>{expensiveProducts.map((product) => /* ... */)}</div>
}
// ✅ BIEN: Transformar datos directamente
function ProductList({ products }) {
const expensiveProducts = products.filter((product) => product.price > 100)
return <div>{expensiveProducts.map((product) => /* ... */)}</div>
}
¿Por qué? Porque no estás haciendo nada fuera del componente. Solo estás transformando datos que ya tienes.
❌ No lo uses para calcular valores derivados
Si un valor se puede calcular a partir del estado o props, calcúlalo directamente:
// ❌ MAL: useEffect innecesario
function ShoppingCart({ items }) {
const [total, setTotal] = useState(0)
useEffect(() => {
const newTotal = items.reduce((sum, item) => sum + item.price, 0)
setTotal(newTotal)
}, [items])
return <div>Total: ${total}</div>
}
// ✅ BIEN: Cálculo directo
function ShoppingCart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
return <div>Total: ${total}</div>
}
❌ No lo uses para inicializar estado
Si solo necesitas inicializar un estado una vez, usa el valor inicial de useState:
// ❌ MAL: useEffect para inicializar
function TodoList() {
const [todos, setTodos] = useState([])
useEffect(() => {
setTodos(['Aprender React', 'Construir proyecto'])
}, [])
return <ul>{todos.map((todo) => /* ... */)}</ul>
}
// ✅ BIEN: Valor inicial en useState
function TodoList() {
const [todos, setTodos] = useState(['Aprender React', 'Construir proyecto'])
return <ul>{todos.map((todo) => /* ... */)}</ul>
}
✅ Cuándo SÍ debes usar useEffect
Usa useEffect cuando necesites hacer algo que no puede ejecutarse durante el render porque afecta algo externo al componente.
1. Fetching de datos
Uno de los casos más comunes es hacer peticiones a APIs:
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
setError(null)
fetch(`https://api.example.com/users/${userId}`)
.then((response) => {
if (!response.ok) throw new Error('Error al cargar usuario')
return response.json()
})
.then((data) => {
setUser(data)
setLoading(false)
})
.catch((error) => {
setError(error.message)
setLoading(false)
})
}, [userId])
if (loading) return <div>Cargando...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>Usuario no encontrado</div>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
¿Por qué necesitamos useEffect aquí?
- No podemos hacer
fetchdurante el render porque es asíncrono - El fetch afecta algo externo (la red)
- Queremos ejecutarlo cuando cambia
userId
Igualmente, existen bibliotecas que simplifican mucho este proceso, como TanStack Query, y no necesitarás usar entonces
useEffect.
2. Modificar el DOM directamente
Cuando necesitas interactuar con APIs del navegador como document:
function DocumentTitle({ title }) {
useEffect(() => {
// Cambiar el título de la pestaña
document.title = title
}, [title])
return <div>La página ahora se llama: {title}</div>
}
Otros ejemplos de modificación del DOM:
function AutoFocusInput() {
const inputRef = useRef(null)
useEffect(() => {
// Hacer focus en el input cuando se monta el componente
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}
3. Suscripciones a eventos
Este es uno de los casos más importantes y donde necesitarás la función de limpieza.
Ejemplo: Detectar el tamaño de la ventana
Vamos a crear un componente que muestra el ancho de la ventana en tiempo real:
import { useState, useEffect } from 'react'
function WindowSize() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
useEffect(() => {
// Handler que se ejecuta cuando la ventana cambia de tamaño
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
// Suscribirse al evento resize
window.addEventListener('resize', handleResize)
// Función de limpieza: desuscribirse cuando el componente se desmonte
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<div>
<h2>Ancho de la ventana: {windowWidth}px</h2>
<p>Intenta redimensionar la ventana y verás el valor actualizarse</p>
</div>
)
}
¿Qué está pasando aquí?
- Cuando el componente se monta,
useEffectse ejecuta - Añadimos un listener al evento
resizede la ventana - Cada vez que redimensionas la ventana, se ejecuta
handleResize handleResizeactualiza el estado con el nuevo ancho- Cuando el componente se desmonta, la función de limpieza elimina el listener
La función de limpieza (cleanup function)
La función de limpieza es el return dentro de useEffect. Es esencial para evitar memory leaks y bugs.
¿Por qué necesitamos limpiar?
Imagina que tienes este componente:
function LiveCounter() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
// ❌ SIN LIMPIEZA: El interval seguirá ejecutándose aunque el componente se desmonte
}, [])
return <div>Contador: {count}</div>
}
Problema: Si el componente se desmonta, el interval seguirá ejecutándose en segundo plano, intentando actualizar un componente que ya no existe. Esto es un memory leak.
✅ Solución: Limpiar el interval
function LiveCounter() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
// ✅ CON LIMPIEZA: Detener el interval cuando el componente se desmonte
return () => {
clearInterval(interval)
}
}, [])
return <div>Contador: {count}</div>
}
Cuándo se ejecuta la función de limpieza
La función de limpieza se ejecuta en dos momentos:
- Cuando el componente se desmonta (desaparece de la pantalla)
- Antes de ejecutar el efecto nuevamente (cuando cambian las dependencias)
function Timer({ delay }) {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`Configurando timer con delay: ${delay}ms`)
const interval = setInterval(() => {
setCount((prev) => prev + 1)
}, delay)
return () => {
console.log(`Limpiando timer con delay: ${delay}ms`)
clearInterval(interval)
}
}, [delay])
return <div>Contador: {count}</div>
}
Si cambias el delay de 1000ms a 500ms:
1. Se ejecuta la función de limpieza (clearInterval del anterior)
2. Se ejecuta el nuevo efecto (nuevo setInterval con 500ms)
Múltiples efectos en un componente
Un componente puede (y debería) tener múltiples efectos si hacen cosas diferentes. No intentes meter todo en un solo useEffect.
function UserDashboard({ userId }) {
const [user, setUser] = useState(null)
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
// Efecto 1: Cargar datos del usuario
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then(setUser)
}, [userId])
// Efecto 2: Suscribirse al resize de la ventana
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Efecto 3: Actualizar el título del documento
useEffect(() => {
if (user) {
document.title = `Dashboard de ${user.name}`
}
}, [user])
return (
<div>
<h1>{user?.name}</h1>
<p>Ancho: {windowWidth}px</p>
</div>
)
}
¿Por qué separarlos?
- ✅ Cada efecto tiene sus propias dependencias
- ✅ Es más fácil de leer y mantener
- ✅ Las limpiezas no se mezclan
- ✅ Puedes desactivar/modificar efectos independientemente
Ejemplo práctico completo: Buscador con resize
Vamos a crear un ejemplo que combina varios conceptos:
import { useState, useEffect } from 'react'
function SearchWithResize() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
const [isMobile, setIsMobile] = useState(false)
// Efecto 1: Buscar cuando cambia el query
useEffect(() => {
if (query === '') {
setResults([])
return
}
const timeoutId = setTimeout(() => {
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then(setResults)
}, 300) // Debounce de 300ms
// Limpiar el timeout si el usuario sigue escribiendo
return () => clearTimeout(timeoutId)
}, [query])
// Efecto 2: Detectar el tamaño de la ventana
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Efecto 3: Calcular si es móvil basado en el ancho
useEffect(() => {
setIsMobile(windowWidth < 768)
}, [windowWidth])
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
<p>
Ancho: {windowWidth}px - {isMobile ? 'Móvil' : 'Escritorio'}
</p>
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
)
}
Casos de uso comunes de limpieza
1. Event listeners
useEffect(() => {
const handleClick = () => console.log('Click!')
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [])
2. Intervals y Timeouts
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick')
}, 1000)
return () => clearInterval(interval)
}, [])
3. Suscripciones a WebSockets
useEffect(() => {
const ws = new WebSocket('ws://example.com')
ws.onmessage = (event) => {
console.log('Mensaje:', event.data)
}
return () => ws.close()
}, [])
4. Librerías externas
useEffect(() => {
const chart = initializeChart('#chart')
return () => chart.destroy()
}, [])
Nuevas alternativas a useEffect para fetching
En el video mencioné que existen nuevas formas de hacer fetching sin useEffect. Estas son algunas:
1. React Server Components
En frameworks modernos como Next.js 13+, puedes hacer fetching directamente en el componente:
// Componente de servidor (Next.js)
async function UserProfile({ userId }) {
const user = await fetch(`/api/users/${userId}`).then((res) => res.json())
return <div>{user.name}</div>
}
2. use Hook (React 19)
React 19 introduce el hook use para leer promesas:
import { use } from 'react'
function UserProfile({ userPromise }) {
const user = use(userPromise)
return <div>{user.name}</div>
}
3. Librerías especializadas
- React Query / TanStack Query: Gestión avanzada de estado asíncrono
- SWR: Librería de fetching de Vercel
- RTK Query: Solución de Redux Toolkit
Estas librerías manejan automáticamente:
- Caché
- Revalidación
- Loading states
- Error handling
- Refetching
Resumen de cuándo usar useEffect
| Caso de uso | ¿Usar useEffect? | ¿Por qué? |
|---|---|---|
| Transformar datos | ❌ No | Hazlo directamente en el cuerpo del componente |
| Calcular valores derivados | ❌ No | No es un efecto secundario |
| Inicializar estado | ❌ No | Usa el valor inicial de useState |
| Fetching de datos | ✅ Sí | Es una operación asíncrona externa |
| Modificar el DOM (document.title) | ✅ Sí | Afecta algo externo al componente |
| Suscribirse a eventos | ✅ Sí | Necesitas limpiar la suscripción |
| Timers (setInterval, setTimeout) | ✅ Sí | Necesitas limpiar el timer |
| WebSockets o conexiones persistentes | ✅ Sí | Necesitas cerrar la conexión al desmontar |
Reglas importantes
- Un efecto, una responsabilidad: Si un efecto hace múltiples cosas sin relación, divídelo
- Siempre limpia lo que creas: Event listeners, intervals, suscripciones
- Piensa si realmente necesitas un efecto: Muchas veces puedes evitarlo
- Las dependencias deben estar completas: Incluye todo lo que uses dentro del efecto
- La limpieza debe deshacer lo que hizo el efecto: Desuscribirse, cancelar, cerrar
Conclusión
useEffect es una herramienta poderosa, pero no debes usarla para todo. Úsala cuando realmente necesites:
- 🌐 Comunicarte con sistemas externos (APIs, navegador)
- 🔔 Suscribirte a eventos que necesitan limpieza
- ⏱️ Crear timers o conexiones persistentes
Recuerda: si puedes hacerlo sin useEffect, no uses useEffect.