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 fetch durante 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í?

  1. Cuando el componente se monta, useEffect se ejecuta
  2. Añadimos un listener al evento resize de la ventana
  3. Cada vez que redimensionas la ventana, se ejecuta handleResize
  4. handleResize actualiza el estado con el nuevo ancho
  5. 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:

  1. Cuando el componente se desmonta (desaparece de la pantalla)
  2. 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❌ NoHazlo directamente en el cuerpo del componente
Calcular valores derivados❌ NoNo es un efecto secundario
Inicializar estado❌ NoUsa 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

  1. Un efecto, una responsabilidad: Si un efecto hace múltiples cosas sin relación, divídelo
  2. Siempre limpia lo que creas: Event listeners, intervals, suscripciones
  3. Piensa si realmente necesitas un efecto: Muchas veces puedes evitarlo
  4. Las dependencias deben estar completas: Incluye todo lo que uses dentro del efecto
  5. 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.