DEV Community

Cover image for 🔹 `useEffect` – Efectos secundarios y ciclo de vida 2/8
Pedro Alvarado
Pedro Alvarado

Posted on • Edited on

🔹 `useEffect` – Efectos secundarios y ciclo de vida 2/8

📋 Tabla de contenidos

  1. useState – El fundamento del estado
  2. useEffect – Efectos secundarios y ciclo de vida
  3. useContext – Compartir estado sin props
  4. useReducer – Lógica de Estado Compleja y predecible
  5. useRef – Referencias, Almacenamiento y Escape
  6. useMemo y useCallback – Optimizando el Rendimiento
  7. Custom Hooks – Creando Lógica Reutilizable
  8. Errores Comunes y Soluciones

¿Qué es y para qué sirve?

useEffect conecta tus componentes con sistemas externos. Permite ejecutar "efectos secundarios" después de que el componente se haya renderizado. Un efecto secundario es cualquier código que afecta a algo fuera del propio componente, como:

  • Llamadas a una API para buscar datos.
  • Suscripciones a eventos (ej. window.addEventListener).
  • Manipulación manual del DOM.
  • Timers como setInterval o setTimeout.

useEffect reemplaza a los métodos de ciclo de vida componentDidMount, componentDidUpdate y componentWillUnmount de los componentes de clase.

Sintaxis y el array de dependencias

useEffect(() => {
  // ...tu código del efecto...

  return () => {
    // ...función de limpieza (opcional)...
  };
}, [dependencias]);
Enter fullscreen mode Exit fullscreen mode

La parte más importante de useEffect es el array de dependencias. Controla cuándo se ejecuta el efecto:

  1. [dependencia1, dependencia2]: El efecto se ejecuta solo si alguna de las dependencias ha cambiado desde el último render. Este es el caso más común.
  2. [] (array vacío): El efecto se ejecuta una sola vez, justo después del renderizado inicial (equivalente a componentDidMount). Perfecto para inicializaciones o fetching de datos que no cambian.
  3. No se proporciona el array: El efecto se ejecuta después de cada renderizado. ¡Cuidado! Esto puede causar bucles infinitos si el efecto actualiza el estado.

La función de limpieza (return) es opcional y se ejecuta:

  • Antes de que el efecto se vuelva a ejecutar (si una dependencia cambió).
  • Cuando el componente se desmonta (se elimina de la pantalla).

Es crucial para limpiar suscripciones, timers o listeners y evitar fugas de memoria.


Ejemplo práctico detallado: fetching de datos con estado de carga y error

Este es el caso de uso más común para useEffect. Vamos a buscar datos de un Pokémon y a manejar los estados intermedios.

import React, { useState, useEffect } from 'react';

function PokemonInfo({ pokemonName }) {
  const [pokemon, setPokemon] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!pokemonName) return;

    // Reseteamos los estados al empezar una nueva búsqueda
    setLoading(true);
    setError(null);
    setPokemon(null);

    fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`)
      .then(response => {
        if (!response.ok) {
          throw new Error('No se encontró el Pokémon');
        }
        return response.json();
      })
      .then(data => {
        setPokemon(data);
      })
      .catch(error => {
        setError(error.message);
      })
      .finally(() => {
        setLoading(false);
      });

  }, [pokemonName]); // El efecto depende del nombre del Pokémon

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

  return (
    <div>
      {pokemon && (
        <>
          <h3>{pokemon.name}</h3>
          <img src={pokemon.sprites.front_default} alt={pokemon.name} />
        </>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🚀 Ver el código interactivo en CodeSandbox Haz clic para abrir el sandbox y experimentar con el código


✅ Buenas practicas y patrones comunes

  1. Un efecto, una responsabilidad: Si tienes lógicas no relacionadas, sepáralas en varios useEffect. Uno para fetching, otro para un event listener, etc. Esto hace el código más limpio.
  2. Limpia siempre tus efectos: Si te suscribes a algo, crea un timer o un listener, siempre devuelve una función de limpieza para cancelarlo.
  3. No olvides las dependencias: El linter de React te suele avisar si usas una variable o función dentro del efecto pero no la incluyes en el array. ¡No lo ignores! Omitir dependencias puede causar bugs muy difíciles de rastrear.
  4. Funciones en el array de dependencias: Si una función se usa dentro de useEffect, debe estar en el array. Pero si esa función se redefine en cada render, causará que el efecto se ejecute siempre. La solución es definir la función dentro del useEffect o memorizarla con useCallback.

🚨 Errores comunes y cómo evitarlos

  • Error: El bucle infinito.

    // MAL ❌
    const [count, setCount] = useState(0);
    useEffect(() => {
      // Este efecto cambia el estado, lo que causa un re-render,
      // lo que vuelve a ejecutar el efecto, y así hasta el infinito.
      setCount(count + 1);
    }); // Sin array de dependencias
    
    • Solución: Añade un array de dependencias. Si quieres que se ejecute solo una vez, que sea []. Si quieres que reaccione a un cambio, incluye la dependencia [otraVariable].
  • Error: Datos "viejos" (stale state) por dependencias faltantes.

    // MAL ❌
    const [count, setCount] = useState(0);
    useEffect(() => {
      // Este intervalo siempre "verá" count como 0, porque la función
      // del intervalo se creó en el primer render y nunca se actualizó.
      const intervalId = setInterval(() => {
        console.log(`El contador es: ${count}`);
      }, 1000);
      return () => clearInterval(intervalId);
    }, []); // El array vacío le dice a React: "nunca vuelvas a ejecutar esto"
    
    • Solución 1: Añadir la dependencia.

      // BIEN ✅ (pero recrea el intervalo cada vez que count cambia)
      useEffect(() => { ... }, [count]);
      
    • Solución 2: Usar la actualización funcional (mejor en este caso).

      // EXCELENTE ✅
      // Dentro de `setCount`, React siempre te da el valor más reciente.
      // No necesitamos `count` como dependencia.
      useEffect(() => {
        const intervalId = setInterval(() => {
           setCount(c => c + 1); // usamos la versión más reciente del estado
        }, 1000);
        return () => clearInterval(intervalId);
      }, []);
      

🚀 Retos prácticos

  • Temporizador con Start/Stop/Reset: Usa useEffect y useState para crear un cronómetro funcional.

💻 Código base (haz clic para mostrar)
import React, { useState } from "react";

// Reto: Seguir el ratón
// Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.
// Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener.

interface Coordenadas {
  x: number;
  y: number;
}

export default function SeguirRatonReto(): React.ReactElement {
  const [coordenadas, setCoordenadas] = useState<Coordenadas>({ x: 0, y: 0 });

  // TODO: Implementar useEffect para:
  // 1. Agregar un event listener para 'mousemove' en window
  // 2. Actualizar las coordenadas cuando el ratón se mueva
  // 3. Limpiar el event listener cuando el componente se desmonte

  return (
    <div style={{ padding: "20px", height: "100vh" }}>
      <h1>Seguimiento del Ratón</h1>
      <p style={{ fontSize: "1.5rem" }}>
        Coordenadas: X: {coordenadas.x}, Y: {coordenadas.y}
      </p>
      <p style={{ color: "#666" }}>
        Mueve el ratón para ver las coordenadas actualizarse en tiempo real.
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

  • Seguir el ratón Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.

(Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener).

💻 Código base (haz clic para mostrar)
import React, { useState } from "react";

// Reto: Seguir el ratón
// Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real.
// Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener.

interface Coordenadas {
    x: number;
    y: number;
}

export default function SeguirRatonReto(): React.ReactElement {
    const [coordenadas, setCoordenadas] = useState<Coordenadas>({ x: 0, y: 0 });

    // TODO: Implementar useEffect para:
    // 1. Agregar un event listener para 'mousemove' en window
    // 2. Actualizar las coordenadas cuando el ratón se mueva
    // 3. Limpiar el event listener cuando el componente se desmonte

    return (
        <div style={{ padding: "20px", height: "100vh" }}>
            <h1>Seguimiento del Ratón</h1>
            <p style={{ fontSize: "1.5rem" }}>
                Coordenadas: X: {coordenadas.x}, Y: {coordenadas.y}
            </p>
            <p style={{ color: "#666" }}>
                Mueve el ratón para ver las coordenadas actualizarse en tiempo real.
            </p>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

  • Título de la página dinámico: Haz que el título del documento (document.title) se actualice para mostrar el valor de un contador o un input.

💻 Código base (haz clic para mostrar)
import React, { useState } from "react";

// Haz que el título del documento (document.title) se actualice para mostrar
// el valor de un contador o un input.

export default function TituloDinamicoReto(): React.ReactElement {
    const [contador, setContador] = useState<number>(0);
    const [textoInput, setTextoInput] = useState<string>("");

    // TODO: Implementar useEffect para actualizar document.title
    // El título debe cambiar cuando el contador o el input cambien
    // Ejemplo: "Contador: 5" o "Texto: Hola mundo"

    const incrementar = () => {
        setContador(contador + 1);
    };

    const decrementar = () => {
        setContador(contador - 1);
    };

    const reiniciar = () => {
        setContador(0);
    };

    return (
        <div style={{ padding: "20px" }}>
            <h1>Título Dinámico</h1>

            <div style={{ marginBottom: "30px" }}>
                <h2>Contador: {contador}</h2>
                <button onClick={incrementar} style={{ margin: "0 5px" }}>
                    +
                </button>
                <button onClick={decrementar} style={{ margin: "0 5px" }}>
                    -
                </button>
                <button onClick={reiniciar} style={{ margin: "0 5px" }}>
                    Reset
                </button>
            </div>

            <div>
                <h2>Input de Texto</h2>
                <input
                    type="text"
                    value={textoInput}
                    onChange={(e) => setTextoInput(e.target.value)}
                    placeholder="Escribe algo..."
                    style={{ padding: "8px", fontSize: "16px", width: "300px" }}
                />
            </div>

            <p style={{ marginTop: "20px", color: "#666" }}>
                Mira el título de la pestaña del navegador para ver los cambios.
            </p>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

  • Autoguardado: Crea un textarea que guarde su contenido en localStorage automáticamente 2 segundos después de que el usuario deje de escribir.

💻 Código base (haz clic para mostrar)
import React, { useState } from "react";

// Crea un textarea que guarde su contenido en localStorage automáticamente
// 2 segundos después de que el usuario deje de escribir.

export default function AutoguardadoReto(): React.ReactElement {
    const [texto, setTexto] = useState<string>("");
    const [ultimoGuardado, setUltimoGuardado] = useState<Date | null>(null);

    // TODO: Implementar useEffect para autoguardar
    // 1. Cargar el texto guardado de localStorage al montar el componente
    // 2. Usar un setTimeout que se active 2 segundos después de que el usuario deje de escribir
    // 3. Guardar en localStorage con la clave "autoguardado-texto"
    // 4. Actualizar la fecha del último guardado
    // Pista: usar clearTimeout para cancelar el timeout anterior cuando el usuario sigue escribiendo

    return (
        <div style={{ padding: "20px", maxWidth: "600px" }}>
            <h1>Autoguardado en localStorage</h1>

            <textarea
                value={texto}
                onChange={(e) => setTexto(e.target.value)}
                placeholder="Escribe algo... Se guardará automáticamente 2 segundos después de que dejes de escribir."
                style={{
                    width: "100%",
                    height: "200px",
                    padding: "10px",
                    fontSize: "16px",
                    border: "1px solid #ccc",
                    borderRadius: "4px",
                    resize: "vertical"
                }}
            />

            <div style={{ marginTop: "10px", color: "#666" }}>
                {ultimoGuardado && (
                    <p>Último guardado: {ultimoGuardado.toLocaleTimeString()}</p>
                )}
                <p>Caracteres: {texto.length}</p>
            </div>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode


Solución reto 1

🚀 Ver el código interactivo en CodeSandbox Haz clic para abrir el sandbox y experimentar con el código

💡 Tip: Puedes modificar el código en el editor de la izquierda y ver los cambios en tiempo real.

Top comments (0)