Hook useEffect: Efectos secundarios en React

El hook useEffect es uno de los hooks más importantes de React. Te permite ejecutar efectos secundarios en tus componentes, es decir, operaciones que afectan algo fuera del componente como llamadas a APIs, manipulación del DOM, suscripciones a eventos, etc.

En esta clase aprenderás cómo funciona useEffect, cuándo se ejecuta, cómo controlar su ejecución con dependencias y cómo evitar errores comunes como los bucles infinitos.

¿Qué son los efectos secundarios?

Un efecto secundario es cualquier operación que afecta algo fuera del scope del componente:

  • 🌐 Llamadas a APIs externas
  • 📝 Modificar el DOM directamente (document.title, localStorage, etc.)
  • 🔔 Suscribirse a eventos del navegador
  • ⏱️ Configurar timers o intervals
  • 📡 Conectarse a WebSockets

En React, los componentes deben ser funciones puras en su render, pero necesitamos una forma de ejecutar estas operaciones. Para eso existe useEffect.

Sintaxis básica de useEffect

import { useEffect } from 'react'

function MyComponent() {
  useEffect(() => {
    // Código del efecto
    console.log('El efecto se ejecutó')
  })

  return <div>Mi componente</div>
}

¿Cuándo se ejecuta useEffect?

Sin segundo parámetro: En cada render

Si no pasas un segundo parámetro, el efecto se ejecuta en cada render del componente:

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('El componente se renderizó')
  })

  return (
    <div>
      <p>Contador: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  )
}

Cada vez que haces clic en el botón:

1. El estado count cambia
2. El componente se re-renderiza
3. useEffect se ejecuta
4. Se imprime "El componente se renderizó"

⚠️ Cuidado: Esto puede causar problemas de rendimiento si el efecto es costoso.

El array de dependencias

El segundo parámetro de useEffect es un array de dependencias que controla cuándo se ejecuta el efecto:

useEffect(() => {
  // Código del efecto
}, [dependencia1, dependencia2])

Array vacío: Solo en el primer render

Si pasas un array vacío [], el efecto se ejecuta solo una vez cuando el componente se monta:

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('Solo se ejecuta al montar el componente')
  }, [])

  return (
    <div>
      <p>Contador: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  )
}

Ahora, aunque hagas clic 100 veces en el botón, el efecto solo se ejecuta una vez al inicio.

Con dependencias: Solo cuando cambian

Si pasas dependencias específicas, el efecto se ejecuta:

  1. En el primer render
  2. Cada vez que alguna dependencia cambia
function App() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('Miguel')

  useEffect(() => {
    console.log('El contador cambió:', count)
  }, [count])

  return (
    <div>
      <p>Contador: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>

      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  )
}

En este ejemplo:

  • ✅ El efecto se ejecuta cuando count cambia
  • ❌ El efecto NO se ejecuta cuando name cambia
  • ✅ El efecto se ejecuta en el primer render

Múltiples dependencias

Puedes tener múltiples dependencias. El efecto se ejecutará si cualquiera de ellas cambia:

function JobListings() {
  const [currentPage, setCurrentPage] = useState(1)
  const [textToFilter, setTextToFilter] = useState('')

  useEffect(() => {
    console.log('Página o filtro cambiaron')
    console.log('Página:', currentPage)
    console.log('Filtro:', textToFilter)
  }, [currentPage, textToFilter])

  return (
    <div>
      <input
        value={textToFilter}
        onChange={(e) => setTextToFilter(e.target.value)}
        placeholder="Filtrar trabajos..."
      />

      <button onClick={() => setCurrentPage(currentPage + 1)}>Siguiente página</button>
    </div>
  )
}

El efecto se ejecuta cuando:

  • ✅ Cambias el texto del filtro
  • ✅ Cambias de página
  • ✅ El componente se monta por primera vez

Evitar bucles infinitos ⚠️

Uno de los errores más comunes con useEffect es crear bucles infinitos. Esto ocurre cuando el efecto modifica una de sus propias dependencias:

❌ Mal: Bucle infinito

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 🚨 ¡BUCLE INFINITO!
    setCount(count + 1)
  }, [count])

  return <div>Contador: {count}</div>
}

¿Qué pasa?

1. El componente se monta → useEffect se ejecuta
2. setCount(count + 1) → count cambia
3. count cambió y es una dependencia → useEffect se ejecuta
4. setCount(count + 1) → count cambia
5. count cambió y es una dependencia → useEffect se ejecuta
6. ... infinito

✅ Solución 1: Eliminar la dependencia

Si no necesitas que el efecto se ejecute cuando cambia count, elimínalo de las dependencias:

useEffect(() => {
  // Solo se ejecuta una vez
  setCount(1)
}, [])

✅ Solución 2: Usar la función callback de setState

Si necesitas el valor anterior, usa la forma funcional de setState:

useEffect(() => {
  // Correcto: no depende de count
  setCount((prevCount) => prevCount + 1)
}, [])

✅ Solución 3: No modifiques las dependencias

Si count es una dependencia, no lo modifiques dentro del efecto:

useEffect(() => {
  // Solo leer count, no modificarlo
  console.log('El contador es:', count)
  document.title = `Contador: ${count}`
}, [count])

Ejemplo práctico: Actualizar el título del documento

Vamos a crear un efecto que cambia el título de la pestaña del navegador según los filtros y la página actual:

function App() {
  const [currentPage, setCurrentPage] = useState(1)
  const [textToFilter, setTextToFilter] = useState('')
  const [jobsData, setJobsData] = useState([])

  // Filtrar trabajos
  const jobsFiltered =
    textToFilter === ''
      ? jobsData
      : jobsData.filter((job) => job.title.toLowerCase().includes(textToFilter.toLowerCase()))

  // Efecto para actualizar el título
  useEffect(() => {
    const jobCount = jobsFiltered.length

    if (textToFilter === '') {
      document.title = `DevJobs - Página ${currentPage}`
    } else {
      document.title = `${jobCount} trabajos de "${textToFilter}" - Página ${currentPage}`
    }
  }, [jobsFiltered.length, currentPage, textToFilter])

  return (
    <div>
      <input
        value={textToFilter}
        onChange={(e) => setTextToFilter(e.target.value)}
        placeholder="Filtrar trabajos..."
      />

      <div>
        {jobsFiltered.map((job) => (
          <div key={job.id}>{job.title}</div>
        ))}
      </div>

      <button onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
        Anterior
      </button>

      <span>Página {currentPage}</span>

      <button onClick={() => setCurrentPage(currentPage + 1)}>Siguiente</button>
    </div>
  )
}

Ahora, cuando:

  • 🔍 Escribes en el filtro → el título se actualiza con el número de resultados
  • 📄 Cambias de página → el título muestra la página actual
  • 🧹 Borras el filtro → el título vuelve a mostrar solo la página

Reglas importantes de useEffect

1. Siempre se ejecuta al menos una vez

El efecto siempre se ejecuta en el primer render, sin importar las dependencias:

useEffect(() => {
  console.log('Se ejecuta en el primer render')
}, [count])

Aunque count no haya cambiado, el efecto se ejecuta al montar el componente.

2. Las dependencias deben incluir todo lo que uses

Si usas variables, props o estado dentro del efecto, debes incluirlos en las dependencias:

// ❌ Mal: falta textToFilter en las dependencias
useEffect(() => {
  console.log('Filtro:', textToFilter)
}, [])

// ✅ Bien: incluimos todas las variables que usamos
useEffect(() => {
  console.log('Filtro:', textToFilter)
}, [textToFilter])

3. No uses objetos o arrays como dependencias directamente

Los objetos y arrays se crean de nuevo en cada render, lo que puede causar que el efecto se ejecute más de lo esperado:

const filters = { technology: 'react', location: 'remote' }

// ❌ Mal: filters es un objeto nuevo en cada render
useEffect(() => {
  console.log('Filtros:', filters)
}, [filters])

Solución: Usa las propiedades individuales como dependencias:

// ✅ Bien: usamos las propiedades
useEffect(() => {
  console.log('Filtros:', filters.technology, filters.location)
}, [filters.technology, filters.location])

4. Evita modificar las dependencias dentro del efecto

Como vimos antes, esto puede causar bucles infinitos:

// ❌ Mal: modifica su propia dependencia
useEffect(() => {
  setCount(count + 1)
}, [count])

// ✅ Bien: no modifica sus dependencias
useEffect(() => {
  console.log('Count:', count)
}, [count])

Resumen de la matriz de dependencias

Dependencias¿Cuándo se ejecuta el efecto?
Sin segundo parámetroEn cada render
[] (array vacío)Solo una vez (al montar)
[dep1]Al montar + cada vez que dep1 cambie
[dep1, dep2]Al montar + cada vez que dep1 o dep2 cambien

Conclusión

El hook useEffect es esencial para ejecutar efectos secundarios en React:

  • 🎯 Controla cuándo se ejecuta con el array de dependencias
  • 🔄 Evita bucles infinitos no modificando las dependencias dentro del efecto
  • 🧹 Haz cleanup cuando sea necesario
  • 📦 Domina la matriz de dependencias para evitar bugs

En las próximas clases veremos hooks más avanzados y aprenderemos a crear nuestros propios custom hooks para reutilizar lógica de efectos.

💡 Recuerda: El efecto siempre se ejecuta al menos una vez (en el primer render), y después solo según las dependencias que le indiques.