useEffect Avanzado y Patrones
Volver a clases
●●Intermedio

useEffect Avanzado y Patrones

120 min

Race conditions, debouncing, polling y organización de efectos.

Race conditions y abort

  • Cuando las respuestas llegan en distinto orden, el estado queda desfasado.
  • Se soluciona con AbortController o flags de cancelación.

Debouncing

  • Evita solicitudes en cada tecla y reduce carga en el servidor.
  • Se implementa con un hook que retrasa el valor.

Polling y refetch

  • Actualiza datos cada cierto tiempo o bajo demanda.
  • Útil para dashboards o notificaciones.

Separación de efectos

  • Un efecto por responsabilidad mejora el mantenimiento.
  • Ayuda a manejar dependencias sin errores.

Dependencias, cierres y estado desactualizado

  • El efecto captura el estado/props del render en el que se definio.
  • Si usas valores viejos dentro del efecto, aparece el problema de "stale closure".
  • Solucion: agrega dependencias correctas o usa useRef para guardar valores mutables.

Async en useEffect

  • No declares useEffect como async directamente.
  • Define una funcion interna async y manejala con try/catch.
  • Usa AbortController o un flag ignore para evitar actualizar estado en un efecto cancelado.

Evitar loops de render

  • Si un efecto actualiza un estado que es dependencia directa, puede generar loops.
  • Verifica si la actualizacion es realmente necesaria antes de llamar a setState.
  • Usa condiciones o compara valores previos.

Integrar eventos del DOM

  • Registra listeners en montaje y limpia en desmontaje.
  • Los handlers deben ser estables para evitar duplicados.

Cuándo crear un custom hook

  • Si un efecto se repite en varios componentes, extraelo a un hook.
  • Encapsula el estado, el efecto y la interfaz publica del comportamiento.

Ejemplos de Código

5 ejemplos

Cancelación de requests

javascript
1import { useEffect, useState } from "react";
2
3function Buscar({ query }) {
4  const [data, setData] = useState([]);
5
6  useEffect(() => {
7    const controller = new AbortController();
8
9    fetch(`/api/search?q=${query}`, { signal: controller.signal })
10      .then((res) => res.json())
11      .then((json) => setData(json))
12      .catch((error) => {
13        if (error.name !== "AbortError") console.error(error);
14      });
15
16    return () => controller.abort();
17  }, [query]);
18
19  return <ul>{data.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
20}

Debounce reutilizable

javascript
1import { useEffect, useState } from "react";
2
3function useDebounce(value, delay) {
4  const [debounced, setDebounced] = useState(value);
5
6  useEffect(() => {
7    const id = setTimeout(() => setDebounced(value), delay);
8    return () => clearTimeout(id);
9  }, [value, delay]);
10
11  return debounced;
12}

Polling con visibilidad

javascript
1import { useEffect, useState } from "react";
2
3function Stats() {
4  const [stats, setStats] = useState(null);
5  const [visible, setVisible] = useState(!document.hidden);
6
7  useEffect(() => {
8    const onVisibility = () => setVisible(!document.hidden);
9    document.addEventListener("visibilitychange", onVisibility);
10    return () => document.removeEventListener("visibilitychange", onVisibility);
11  }, []);
12
13  useEffect(() => {
14    if (!visible) return;
15
16    const cargar = async () => {
17      const res = await fetch("/api/stats");
18      setStats(await res.json());
19    };
20
21    cargar();
22    const id = setInterval(cargar, 5000);
23    return () => clearInterval(id);
24  }, [visible]);
25
26  return <pre>{JSON.stringify(stats, null, 2)}</pre>;
27}

Ref para valores actuales

javascript
1import { useEffect, useRef } from "react";
2
3function Logger({ value }) {
4  const latest = useRef(value);
5
6  useEffect(() => {
7    latest.current = value;
8  }, [value]);
9
10  useEffect(() => {
11    const id = setInterval(() => {
12      console.log("Ultimo valor:", latest.current);
13    }, 1000);
14    return () => clearInterval(id);
15  }, []);
16
17  return null;
18}

Async con cancelacion

javascript
1import { useEffect, useState } from "react";
2
3function Detalle({ id }) {
4  const [data, setData] = useState(null);
5
6  useEffect(() => {
7    let ignore = false;
8
9    const cargar = async () => {
10      const res = await fetch(`/api/items/${id}`);
11      const json = await res.json();
12      if (!ignore) setData(json);
13    };
14
15    if (id) cargar();
16    return () => {
17      ignore = true;
18    };
19  }, [id]);
20
21  return <pre>{JSON.stringify(data, null, 2)}</pre>;
22}

Recursos

4 recursos disponibles

¡Hora de Practicar!

PrácticaPrincipiante15 min🟡 IA con bitácora

Práctica guiada - Buscador con debounce

Práctica

Input de búsqueda que dispara requests con debounce y cancelación de peticiones.

Desafío de Código

EjercicioPrincipiante15 min🔴 Sin IA

Ejercicios - Patrones con efectos

Ejercicios

(1) Refetch manual con botón, (2) polling configurable, (3) separar efectos por responsabilidad.

Documentación Oficial

DocumentaciónPrincipiante15 min

A Complete Guide to useEffect

Guía avanzada para entender dependencias y cierres.

Reto de Lectura

Reto de LecturaPrincipiante35 min🔴 Sin IA

Reto de Lectura — El buscador que se contradice

Un buscador con tres bugs que solo aparecen cuando la red va lenta o el usuario teclea rápido: dependencias mentirosas, sin cleanup y una race condition. Los tres son invisibles en la demo.

Este buscador funciona perfecto en la demo del profesor: escribes, esperas, aparecen resultados. En producción tiene tres bugs. Encuéntralos sin ejecutar el código, y describe el escenario exacto en que muerde cada uno.

jsx
1function BuscadorProductos({ categoria }) {
2  const [query, setQuery] = useState("");
3  const [resultados, setResultados] = useState([]);
4  const [cargando, setCargando] = useState(false);
5
6  useEffect(() => {
7    setCargando(true);
8
9    const t = setTimeout(() => {
10      fetch(`/api/productos?cat=${categoria}&q=${query}`)
11        .then((r) => r.json())
12        .then((datos) => {
13          setResultados(datos);
14          setCargando(false);
15        });
16    }, 300);
17  }, [query]);
18
19  return (
20    <div>
21      <input value={query} onChange={(e) => setQuery(e.target.value)} />
22      {cargando ? <Spinner /> : <Lista items={resultados} />}
23    </div>
24  );
25}

Preguntas:

  1. El usuario está en la categoría "Ropa", busca "azul", y luego cambia a la categoría "Zapatos". ¿Qué resultados ve? ¿Por qué?
  2. El usuario teclea "camisa" (6 letras, rápido). ¿Cuántos setTimeout se crearon? ¿Cuántas peticiones salen? ¿Para qué servía el 300?
  3. Con la red lenta: el usuario escribe "ca", espera, y completa "camisa". La respuesta de "ca" llega después que la de "camisa". ¿Qué ve en pantalla y qué dice el input?
  4. ¿Qué pasa si el usuario cierra el buscador mientras hay una petición en vuelo?
  5. Escribe la versión corregida.