Efectos secundarios y solicitudes de datos — useEffect

En este capítulo aprenderás el Hook de efectos secundarios más importante de React —useEffect—, y cargarás datos de blog desde un archivo JSON con fetch en el momento adecuado.


¿Qué es un efecto secundario?

En React,un efecto secundario (Side Effect)se refiere a aquellas operaciones que no están relacionadas con el renderizado:

  • Solicitar datos al servidor
  • Operar con localStorage
  • Configurar un temporizador setInterval
  • Modificar el DOM manualmente
  • Suscribirse a eventos externos

Los efectos secundarios no pueden ejecutarse directamente en el cuerpo de la función del componente (haría que se ejecutaran en cada renderizado), deben colocarse enuseEffect.


Uso básico de useEffect

Ejemplo

import { useState, useEffect } from 'react'

function HomePage() {
  const [message, setMessage] = useState('Cargando...')

  // useEffect(fn, deps): ejecuta el efecto secundario después de que el componente se renderice
  useEffect(() => {
    console.log('El componente se ha montado en el DOM, ya se pueden ejecutar efectos secundarios')
    setMessage('¡Carga de datos completada!')
  }, [])  // Array de dependencias vacío = se ejecuta solo una vez después del primer renderizado

  return <p>{message}</p>
}

Tres formas de escribir el array de dependencias

FormaMomento de ejecuciónCaso de uso
useEffect(fn)Se ejecuta después de cada renderizadoSe usa muy raramente, puede causar bucles infinitos
useEffect(fn, [])Se ejecuta solo una vez después del primer renderizadoCarga de datos, inicialización de librerías de terceros
useEffect(fn, [dep])Se ejecuta después del primer renderizado + cuando dep cambiaEscuchar cambios de datos específicos y ejecutar operaciones de respuesta

Diferencia entre omitir el array de dependencias vs array vacío:useEffect(fn)Se ejecuta después de cada renderizado;useEffect(fn, [])Se ejecuta solo una vez después del primer renderizado. El primero puede causar fácilmente bucles infinitos (setState en effect → dispara renderizado → ejecuta effect nuevamente → ...), casi nunca se usa.


Cargar datos JSON con fetch

Al igual que en Vue3, colocamos los datos de los artículos enpublic/posts.jsonel archivo.

Primer paso: crear el archivo de datos

Ejemplo

// Ruta del archivo: public/posts.json
[
  {
    "id": 1,
"title": "Guía completa de introducción a React",
"summary": "Aprende React Hooks desde cero, cubriendo conceptos clave como useState, useEffect.",
"content": "<h2>¿Por qué aprender React?</h2><p>React es uno de los frameworks frontend más populares actualmente...</p>",
    "category": "React",
    "date": "2024-05-10"
  },
  {
    "id": 2,
"title": "Programación asíncrona en JavaScript en detalle",
"summary": "Entiende de una vez Promise, async/await, el bucle de eventos y la cola de microtareas.",
"content": "<h2>¿Qué es la asincronía?</h2><p>JS es de un solo hilo...</p>",
    "category": "JavaScript",
    "date": "2024-05-08"
  },
  {
    "id": 3,
"title": "Práctica de diseño con CSS Grid",
"summary": "Implementa fácilmente diseños responsive complejos con CSS Grid.",
"content": "<h2>Introducción a Grid</h2><p>Grid es un sistema de diseño bidimensional...</p>",
    "category": "CSS",
    "date": "2024-05-05"
  }
]

Segundo paso: cargar datos en useEffect

Ejemplo

// Ruta del archivo: src/pages/HomePage.jsx
import { useState, useEffect, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'

function HomePage() {
  const [articles, setArticles] = useState([])   // Datos de los artículos
  const [isLoading, setIsLoading] = useState(true) // Estado de carga
  const [error, setError] = useState(null)        // Mensaje de error
  const [activeCategory, setActiveCategory] = useState('Todos')

  // useEffect carga datos: array de dependencias vacío = se ejecuta solo una vez después del primer renderizado
  useEffect(() => {
    let cancelled = false  // Marcador para evitar setState después de que el componente se desmonte

    async function fetchPosts() {
      setIsLoading(true)
      setError(null)
      try {
        const res = await fetch('/posts.json')
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const data = await res.json()
        if (!cancelled) {
          setArticles(data)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message)
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false)
        }
      }
    }

    fetchPosts()

    // Función de limpieza: establece cancelled en true cuando el componente se desmonta
    return () => { cancelled = true }
  }, [])

  const categories = useMemo(() => {
    return ['Todos', ...new Set(articles.map(a => a.category))]
  }, [articles])

  const filteredArticles = useMemo(() => {
    if (activeCategory === 'Todos') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])

  return (
    <div>
      <h2 className="section-title">Últimos artículos</h2>

      {/* Cargando */}
      {isLoading && <p className="status-msg">Cargando, por favor espere...</p>}

      {/* Error al cargar */}
      {error && (
        <div className="status-msg error">
          <p>Error al cargar:{error}</p>
          <button onClick={() => window.location.reload()}>Reintentar</button>
        </div>
      )}

      {/* Carga de datos completada */}
      {!isLoading && !error && (
        <>
          <CategoryFilter
            categories={categories}
            activeCategory={activeCategory}
            onCategoryChange={setActiveCategory}
          />
          <p className="result-info">común{filteredArticles.length}artículo</p>
          {filteredArticles.length === 0 ? (
            <p className="empty-tip">No hay artículos en esta categoría</p>
          ) : (
            <div className="article-grid">
              {filteredArticles.map(article => (
                <BlogCard key={article.id} {...article} />
              ))}
            </div>
          )}
        </>
      )}
    </div>
  )
}

export default HomePage

cancelledEl marcador es un patrón común en React. Cuando fetch aún está en curso, si el usuario abandona la página (el componente se desmonta), el setState posterior generará una advertencia de error. Establecer en la función cleanupcancelled = truepermite omitir setState después del desmontaje.


Función de limpieza (Cleanup)

La función de retorno de useEffect puede devolver una función, llamadafunción de limpieza。

Se ejecuta en dos momentos:Cuando el componente se desmontayAntes de la próxima ejecución del effect。

Ejemplo

useEffect(() => {
  // Efecto secundario: suscripción a eventos
  const handleScroll = () => console.log('Se ha desplazado')
  window.addEventListener('scroll', handleScroll)

  // Devuelve la función de limpieza
  return () => {
    // Elimina automáticamente el listener de eventos al desmontar el componente, evitando fugas de memoria
    window.removeEventListener('scroll', handleScroll)
  }
}, [])

useEffect(() => {
  // Efecto secundario: configurar un temporizador
  const timer = setInterval(() => {
    console.log('Se ejecuta cada segundo')
  }, 1000)

  return () => clearInterval(timer)  // Limpiar el temporizador
}, [])
Efecto secundario¿Requiere limpieza?Método de limpieza
Solicitud fetchSíUsar el marcador cancelled o AbortController
Escucha de eventosSíremoveEventListener
TemporizadorSíclearInterval / clearTimeout
Restaurar después de modificar el DOMSíRestaurar el estado original en cleanup
Escritura en localStoragenoNo requiere limpieza

Cuatro estados para manejar solicitudes asíncronas

Cualquier solicitud de datos debería cubrir los siguientes cuatro estados de UI:

EstadoCondiciónUI
CargandoisLoading === trueAnimación de loading, pantalla esqueleto
ÉxitoLos datos han sido devueltosRenderizar el contenido normalmente
Errorerror !== nullMensaje de error + botón de reintentar
Datos vacíosLos datos son un array vacíoMensaje de estado vacío

Comparación con el ciclo de vida de Vue3

Vue3ReactDescripción
onMounteduseEffect(fn, [])Se ejecuta después del primer renderizado
watch(keyword, fn)useEffect(fn, [keyword])Responde a cambios de datos específicos
onBeforeUnmountuseEffect 的 cleanupLimpieza antes de que el componente se destruya
No tiene correspondencia directauseEffect(() => { fn; return cleanup }, [dep])Cuando dep cambia, primero cleanup y luego fn

Resumen del capítulo

En este capítulo has dominado el uso principal de useEffect: las tres formas de escribir el array de dependencias, la carga asíncrona de datos con fetch, el manejo de los cuatro estados de UI y el papel de la función de limpieza cleanup.

Ahora los datos del blog se cargan dinámicamente desde un JSON externo, ya no están codificados en JS.

Otras extensiones