29. useEffect

 

Tutorial: 29. useEffect en React

Introducción a useEffect

useEffect es uno de los Hooks más importantes en React que nos permite ejecutar efectos secundarios en nuestros componentes funcionales. Los efectos secundarios son operaciones que pueden incluir peticiones a APIs, suscripciones, manipulación del DOM, etc.


1. Sintaxis Básica

La sintaxis básica de useEffect es:

javascript

useEffect(() => {

  // Código a ejecutar

}, []) // Array de dependencias

Ejemplo visual de la sintaxis:

text

useEffect(()=>{},[])






2. Ejemplo Práctico: Sistema de Likes

Vamos a construir un componente de likes para entender cómo funciona useEffect:

javascript

import { useState, useEffect } from "react";


export function Prueba() {

  const [likes, setLikes] = useState(0);

  

  const darLike = () => {

    setLikes(likes + 1);

  };

  

  // useEffect ejecutando darLike una sola vez

  useEffect(darLike, []);


  return (

    <div>

      <h1>Likes {likes}</h1>

      <button onClick={darLike}>Dar like</button>

    </div>

  );

}

¿Qué hace este código?

  1. Creamos un estado likes que inicia en 0

  2. La función darLike incrementa el contador

  3. El botón ejecuta darLike cuando hacemos clic

  4. useEffect ejecuta darLike una sola vez cuando el componente se monta


3. Herramienta de Desarrollo: React Developer Tools

Para depurar y visualizar mejor nuestros componentes y estados:

  1. Instala React Developer Tools desde la Chrome Web Store

  2. Abre las herramientas de desarrollo (F12)

  3. Ve a la pestaña "Components"

  4. Podrás ver:

    • Todos los componentes renderizados

    • Sus estados y props

    • Cómo se actualizan en tiempo real

Beneficios:

  • Visualizas el estado actual (likes)

  • Ves cómo se modifican los valores

  • Identificas qué componentes se actualizan


4. Casos de Uso Real de useEffect

A. Actualización de Carrito de Compras

javascript

// Cuando añades un producto al carrito

useEffect(() => {

  actualizarContadorCarrito();

}, [productosEnCarrito]); // Se ejecuta cuando productosEnCarrito cambia

B. Peticiones a APIs

javascript

useEffect(() => {

  fetch('/api/datos')

    .then(res => res.json())

    .then(data => setDatos(data));

}, []); // Se ejecuta solo al montar el componente



C. Suscripciones y Eventos

javascript

useEffect(() => {

  const subscription = api.subscribe();

  

  // Función de limpieza

  return () => {

    subscription.unsubscribe();

  };

}, []);


5. El Array de Dependencias []

Ejecutar una sola vez:

javascript

useEffect(() => {

  console.log("Se ejecuta solo al montar el componente");

}, []); // Array vacío

Ejecutar cuando una variable cambia:

javascript

useEffect(() => {

  console.log("Se ejecuta cuando 'likes' cambia");

}, [likes]); // Observa cambios en 'likes'



⚠️ PELIGRO: Bucle infinito

javascript

useEffect(() => {

  setLikes(likes + 1); // ❌ ¡CUIDADO!

}); // Sin array de dependencias = se ejecuta en cada render

¿Por qué ocurre el bucle infinito?

  1. useEffect se ejecuta

  2. Actualiza el estado (setLikes)

  3. El componente se re-renderiza

  4. useEffect se ejecuta de nuevo...

  5. ¡Bucle infinito!


6. Cómo Funciona Internamente

useEffect actúa como un observador:

  1. Observa las variables en el array de dependencias

  2. Compara sus valores actuales con los anteriores

  3. Ejecuta la función si detecta cambios

  4. Opcionalmente ejecuta una función de limpieza






7. Ejemplo Completo: useEffect en Acción

javascript

import { useState, useEffect } from "react";


function ContadorConEfecto() {

  const [contador, setContador] = useState(0);

  const [actualizaciones, setActualizaciones] = useState(0);


  // Efecto que se ejecuta al montar

  useEffect(() => {

    console.log("Componente montado");

    setContador(1); // Inicia en 1

  }, []);


  // Efecto que se ejecuta cuando contador cambia

  useEffect(() => {

    console.log(`Contador cambió a: ${contador}`);

    setActualizaciones(prev => prev + 1);

  }, [contador]); // Dependencia: contador


  return (

    <div>

      <h2>Contador: {contador}</h2>

      <h3>Actualizaciones: {actualizaciones}</h3>

      <button onClick={() => setContador(contador + 1)}>

        Incrementar

      </button>

    </div>

  );

}


8. Buenas Prácticas

  1. Siempre incluye el array de dependencias

  2. Usa funciones de limpieza para suscripciones

  3. Separa efectos por responsabilidad

  4. Evita efectos innecesarios en cada render

  5. Mueve lógica compleja a custom hooks


9. Resumen

Situación

Array de Dependencias

Comportamiento

Ejecutar solo al montar

[]

Una sola vez

Ejecutar cuando X cambie

[variable]

Cuando variable cambia

Ejecutar en cada render

Sin array

En cada actualización (¡cuidado!)

Con limpieza

[] o [deps]

Ejecuta función al desmontar


10. Conclusión

useEffect es una herramienta poderosa para:

  • Sincronizar componentes con sistemas externos

  • Ejecutar código en momentos específicos del ciclo de vida

  • Manejar efectos secundarios de manera controlada

  • Optimizar rendimiento evitando ejecuciones innecesarias

Recuerda: ¡Siempre especifica el array de dependencias! Esto previene bucles infinitos y hace tu código más predecible.


Ejercicio propuesto: Crea un componente que:

  1. Cargue datos de una API al montarse

  2. Actualice un contador cada 5 segundos

  3. Limpie el intervalo al desmontarse

  4. Muestre un mensaje cuando los datos se carguen

useEffect para Llamadas a APIs: Guía Práctica

useEffect es el hook de React que te permite sincronizar tu componente con sistemas externos, como APIs, bases de datos, o cualquier operación que ocurre "fuera" de React.

La idea principal

Cuando necesitas traer datos de una API cuando el componente se monta, o cuando cierta variable cambia, usas useEffect.

Estructura básica:

jsx

useEffect(() => {

  // Código que se ejecuta (efecto)

  

  return () => {

    // Código de limpieza (opcional)

  };

}, [dependencias]); // Array de dependencias







Ejemplo más simple: Traer datos al montar

jsx

import { useState, useEffect } from 'react';


function Usuario() {

  const [usuario, setUsuario] = useState(null);

  const [cargando, setCargando] = useState(true);

  const [error, setError] = useState(null);


  useEffect(() => {

    // Esta función se ejecuta cuando el componente se monta

    const fetchUsuario = async () => {

      try {

        setCargando(true);

        const response = await fetch('https://api.ejemplo.com/usuario/1');

        const data = await response.json();

        setUsuario(data);

      } catch (err) {

        setError('Error al cargar usuario');

      } finally {

        setCargando(false);

      }

    };


    fetchUsuario();

  }, []); // Array vacío = se ejecuta solo al montar


  if (cargando) return <p>Cargando...</p>;

  if (error) return <p>{error}</p>;

  

  return (

    <div>

      <h1>{usuario.nombre}</h1>

      <p>Email: {usuario.email}</p>

    </div>

  );

}

Patrones comunes para APIs

1. Búsqueda con parámetros

jsx

function BuscadorProductos() {

  const [productos, setProductos] = useState([]);

  const [busqueda, setBusqueda] = useState('');

  const [cargando, setCargando] = useState(false);


  // Se ejecuta cuando cambia 'busqueda'

  useEffect(() => {

    // Evitar búsqueda si está vacío

    if (!busqueda.trim()) {

      setProductos([]);

      return;

    }


    const buscarProductos = async () => {

      setCargando(true);

      try {

        const response = await fetch(

          `https://api.ejemplo.com/productos?q=${busqueda}`

        );

        const data = await response.json();

        setProductos(data);

      } catch (error) {

        console.error('Error:', error);

      } finally {

        setCargando(false);

      }

    };


    // Debounce: espera 300ms antes de hacer la petición

    const timer = setTimeout(buscarProductos, 300);

    

    // Cleanup: cancela la petición si el usuario sigue escribiendo

    return () => clearTimeout(timer);

  }, [busqueda]); // Dependencia: se ejecuta cuando 'busqueda' cambia


  return (

    <div>

      <input

        type="text"

        value={busqueda}

        onChange={(e) => setBusqueda(e.target.value)}

        placeholder="Buscar productos..."

      />

      {cargando && <p>Buscando...</p>}

      <ul>

        {productos.map((producto) => (

          <li key={producto.id}>{producto.nombre}</li>

        ))}

      </ul>

    </div>

  );

}

2. Datos que dependen de otros datos

jsx

function PerfilUsuario({ userId }) {

  const [usuario, setUsuario] = useState(null);

  const [posts, setPosts] = useState([]);


  // Traer usuario cuando cambie userId

  useEffect(() => {

    const fetchUsuario = async () => {

      const response = await fetch(

        `https://jsonplaceholder.typicode.com/users/${userId}`

      );

      const data = await response.json();

      setUsuario(data);

    };

    

    fetchUsuario();

  }, [userId]); // Se ejecuta cuando userId cambia


  // Traer posts del usuario cuando usuario esté disponible

  useEffect(() => {

    if (!usuario) return; // No ejecutar si no hay usuario

    

    const fetchPosts = async () => {

      const response = await fetch(

        `https://jsonplaceholder.typicode.com/posts?userId=${userId}`

      );

      const data = await response.json();

      setPosts(data);

    };

    

    fetchPosts();

  }, [usuario, userId]); // Depende de usuario y userId


  // Renderizar...

}



3. POST a una API (crear recursos)

jsx

function FormularioNuevoPost() {

  const [titulo, setTitulo] = useState('');

  const [cuerpo, setCuerpo] = useState('');

  const [enviando, setEnviando] = useState(false);

  const [mensaje, setMensaje] = useState('');


  const handleSubmit = async (e) => {

    e.preventDefault();

    setEnviando(true);

    

    try {

      const response = await fetch('https://jsonplaceholder.typicode.com/posts', {

        method: 'POST',

        headers: {

          'Content-Type': 'application/json',

        },

        body: JSON.stringify({

          title: titulo,

          body: cuerpo,

          userId: 1,

        }),

      });

      

      const data = await response.json();

      setMensaje('¡Post creado con éxito!');

      setTitulo('');

      setCuerpo('');

      

      console.log('Respuesta:', data);

    } catch (error) {

      setMensaje('Error al crear post');

    } finally {

      setEnviando(false);

    }

  };


  // Limpiar mensaje después de 3 segundos

  useEffect(() => {

    if (mensaje) {

      const timer = setTimeout(() => {

        setMensaje('');

      }, 3000);

      

      return () => clearTimeout(timer);

    }

  }, [mensaje]);


  return (

    <form onSubmit={handleSubmit}>

      <input

        value={titulo}

        onChange={(e) => setTitulo(e.target.value)}

        placeholder="Título"

      />

      <textarea

        value={cuerpo}

        onChange={(e) => setCuerpo(e.target.value)}

        placeholder="Contenido"

      />

      <button type="submit" disabled={enviando}>

        {enviando ? 'Enviando...' : 'Crear Post'}

      </button>

      {mensaje && <p>{mensaje}</p>}

    </form>

  );

}






Manejo de errores robusto

function ApiConManejoErrores() {

  const [datos, setDatos] = useState(null);

  const [estado, setEstado] = useState('idle'); // 'idle', 'loading', 'success', 'error'


  useEffect(() => {

    const fetchData = async () => {

      setEstado('loading');

      

      try {

        // Simular error aleatorio

        if (Math.random() > 0.7) {

          throw new Error('Error de conexión simulado');

        }

        

        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');

        

        if (!response.ok) {

          throw new Error(`HTTP error! status: ${response.status}`);

        }

        

        const data = await response.json();

        setDatos(data);

        setEstado('success');

      } catch (error) {

        setEstado('error');

        console.error('Error en fetch:', error);

      }

    };


    fetchData();

  }, []);


  // Renderizar según estado

  switch (estado) {

    case 'idle':

      return null;

    case 'loading':

      return <div>Cargando datos...</div>;

    case 'error':

      return <div>Error al cargar datos. Intenta de nuevo.</div>;

    case 'success':

      return (

        <div>

          <h2>{datos.title}</h2>

          <p>ID: {datos.id}</p>

          <p>Completado: {datos.completed ? 'Sí' : 'No'}</p>

        </div>

      );

    default:

      return null;

  }

}










Buenas prácticas para APIs

1. Abortar peticiones al desmontar

jsx

useEffect(() => {

  const controller = new AbortController();

  const signal = controller.signal;


  const fetchData = async () => {

    try {

      const response = await fetch('https://api.ejemplo.com/datos', { signal });

      // Procesar respuesta

    } catch (error) {

      if (error.name === 'AbortError') {

        console.log('Fetch abortado');

      } else {

        // Manejar otros errores

      }

    }

  };


  fetchData();


  // Cleanup: aborta la petición si el componente se desmonta

  return () => controller.abort();

}, []);





2. Custom Hook reutilizable

// hooks/useApi.js

function useApi(url) {

  const [data, setData] = useState(null);

  const [loading, setLoading] = useState(true);

  const [error, setError] = useState(null);


  useEffect(() => {

    const fetchData = async () => {

      try {

        setLoading(true);

        const response = await fetch(url);

        if (!response.ok) throw new Error('Error en la petición');

        

        const result = await response.json();

        setData(result);

      } catch (err) {

        setError(err.message);

      } finally {

        setLoading(false);

      }

    };


    fetchData();

  }, [url]);


  return { data, loading, error };

}


// Uso en componente

function MiComponente() {

  const { data, loading, error } = useApi('https://jsonplaceholder.typicode.com/users');

  

  if (loading) return <p>Cargando...</p>;

  if (error) return <p>Error: {error}</p>;

  

  return (

    <ul>

      {data?.map(user => (

        <li key={user.id}>{user.name}</li>

      ))}

    </ul>  );}

Reglas de oro para useEffect con APIs

  1. Siempre maneja estados de carga y error

  2. Usa cleanup para abortar peticiones pendientes

  3. No olvides las dependencias en el array

  4. Evita llamadas innecesarias con condiciones

  5. Separa responsabilidades en múltiples useEffect si es necesario

  6. Considera usar una librería como React Query o SWR para casos complejos

Resumen visual

text

useEffect(() => {

  // 1. Iniciar carga

  // 2. Hacer fetch a la API

  // 3. Procesar respuesta

  // 4. Actualizar estado

  

  return () => {

    // Limpiar (abortar fetch, limpiar timeouts)

  };

}, [dependencias]); // ¿Cuándo se ejecuta?

¡Comienza con ejemplos simples y ve incrementando la complejidad! La práctica es clave para dominar useEffect con APIs




Comentarios

Entradas más populares de este blog

11. Creando un proyecto react con create-react-app

38. 4 Routers