Ir al contenido
wannacode

Datos de una API en un componente

En resumen: Los datos de una API se cargan en un componente de React desde useEffect con un estado «cargando», «error» o «listo», y un AbortController en la limpieza detiene la petición obsoleta.

Problema

Manzanas bajo «Bebidas»

El catálogo obtiene de la API los productos de la categoría. Aquí el servidor responde «Frutas y verduras» en 1,5 segundos y las demás categorías en 0,3.

React

Pulsa «Frutas y verduras» y enseguida «Bebidas». Aparecen las bebidas. Un segundo después, manzanas, bananas y tomates ocupan su lugar, mientras que el encabezado y el botón pulsado siguen diciendo «Bebidas». El cliente ve los productos equivocados y no sabe por qué.

La idea

Una petición es un efecto

La petición sale del componente hacia el servidor. Eso la convierte en un efecto secundario, y su sitio está en el efecto, con la categoría como dependencia: cada categoría nueva envía una petición nueva.

Los estados son los de la lección sobre fetch en la página: cargando, error, listo. Aquí son una sola pieza de estado, status, y el componente dibuja la rama que le corresponde, como en la lección sobre dibujo condicional.

useEffect(() => {
async function load() {
setStatus('loading');
try {
const response = await fetch(`/api/products?category=${categoryId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setProducts(await response.json());
setStatus('ready');
} catch (e) {
setStatus('error');
}
}
load();
}, [categoryId]);

La función del efecto no puede ser async: tiene que devolver la limpieza, y una función async devuelve una promesa. Por eso load es una función async aparte dentro del efecto, que se llama enseguida.

status pasa a 'loading' al comienzo de cada carga, no solo la primera vez. Si no, en un cambio la lista antigua se queda bajo el encabezado nuevo hasta que llega la respuesta.

Pruébalo

Carga la categoría

Catalog tiene los estados products y status, pero nada los rellena. Carga los productos de categoryId desde /api/products?category=… y muestra «Cargando…» en cada carga. La comprobación mira los lácteos (5 productos), luego pulsa «Bebidas» y espera «Cargando…» y 4 productos.

React

La idea

La carrera

Cada pulsación cambia la categoría y vuelve a ejecutar el efecto. Cada ejecución envía su propia petición y espera su propia respuesta. Ninguna sabe de las demás.

Las respuestas no llegan en el orden de las peticiones. Puede que el servidor tarde más en buscar la fruta, o que la red frene una de ellas. Así que, cuando el cliente cambia rápido, hay dos peticiones en camino a la vez. Gana la que responde la última, no la que salió la última.

Esto se llama condición de carrera (race condition). No se nota mientras el servidor es rápido y regular. Se nota en una red lenta, en un móvil, en un servidor ocupado.

Míralo

Dos peticiones en camino

Las barras crecen mientras una petición viaja. Primero sin limpieza, luego con ella.

useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const url = `/api/products?category=${categoryId}`;
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setProducts(await response.json());
setStatus('ready');
} catch (e) {
if (e.name === 'AbortError') return;
setStatus('error');
}
}
load();
return () => controller.abort();
}, [categoryId]);
Paso a paso, en palabras
  1. El cliente pulsa «Frutas y verduras». La categoría cambia, el efecto se ejecuta y envía una petición. Esta es lenta.
  2. Antes de que responda, el cliente pulsa «Bebidas». El efecto se ejecuta otra vez y envía una segunda petición. Ahora hay dos peticiones en camino a la vez.
  3. La petición rápida responde primero: la lista muestra las bebidas.
  4. Supongamos que falta la limpieza. La petición lenta responde la última, su ejecución también asigna los productos y bajo «Bebidas» aparece fruta.
  5. Ahora con la limpieza. Cada ejecución del efecto crea su propio AbortController y pasa su signal a fetch.
  6. La nueva categoría llama a la limpieza de la ejecución anterior: abort() detiene la petición lenta. Su fetch lanza AbortError y catch lo omite: no es un error.
  7. Solo responde la petición de las bebidas. La lista y el encabezado coinciden.
  8. La regla: una petición dentro de un efecto lleva su cancelación en la limpieza. Gana la última ejecución, porque todas las anteriores se detuvieron.

Sin limpieza, la pantalla muestra la última respuesta. Con limpieza, la petición antigua se detiene antes de responder, y la última respuesta es siempre de la última categoría.

La idea

Cancelar en la limpieza

El efecto ya tiene un sitio para «detén lo antiguo»: la limpieza. El navegador tiene una herramienta para detener una petición: AbortController.

  1. Cada ejecución del efecto crea su propio controller = new AbortController().
  2. Pasa su controller.signal a fetch: fetch(url, { signal: controller.signal }).
  3. La limpieza llama a controller.abort(). La petición se detiene y await fetch(…) lanza un error llamado AbortError.
  4. catch omite ese error: if (e.name === 'AbortError') return;. La petición no falló; la detuvo el código.
useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const response = await fetch(url, { signal: controller.signal });
…
} catch (e) {
if (e.name === 'AbortError') return;
setStatus('error');
}
}
load();
return () => controller.abort();
}, [categoryId]);

La limpieza se ejecuta también cuando el componente desaparece. El cliente ha dejado el catálogo: la petición se detiene y nadie asigna productos en un componente que ya no existe.

Pruébalo

Detén la petición antigua

Haz que la petición antigua se detenga cuando el cliente cambia rápido. La comprobación pulsa «Frutas y verduras» y enseguida «Bebidas», espera «Cargando…» y las bebidas, deja pasar la respuesta lenta y se asegura de que no haya manzanas y de que no haya aparecido ningún mensaje de error.

React

Pruébalo

Encuentra el error

El cliente cambia rápido y ve un momento «Los productos no se cargaron.», luego llegan los productos. El servidor no devolvió ningún error. ¿Qué línea tiene la culpa?

Elige la línea con el error.

Pruébalo

Completa

¿Qué detiene la petición antigua?

useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal }).then(show);
  return () => controller.();
}, [categoryId]);

La idea

Error e «Intentar de nuevo»

A veces el servidor responde con 500. La comprobación de response.ok lanza, catch asigna 'error' y el componente muestra el mensaje con role="alert". El cliente debería poder intentarlo de nuevo sin recargar la página.

Intentarlo de nuevo es la misma carga. El efecto ya la hace cuando cambia una dependencia. Así que «Intentar de nuevo» cambia una dependencia: un número attempt que solo cuenta los intentos.

const [attempt, setAttempt] = useState(0);
useEffect(() => {
// … la misma carga
}, [categoryId, attempt]);
<button type="button" onClick={() => setAttempt(attempt + 1)}>Intentar de nuevo</button>

La nueva ejecución asigna 'loading', así que el mensaje y el botón desaparecen mientras la petición viaja.

Más precisamente, para curiosos

Bibliotecas de datos. En un proyecto real, la carga rara vez se escribe a mano en cada componente. Hay bibliotecas que hacen esto y más: guardan las respuestas en una caché, no envían dos peticiones iguales, reintentan por su cuenta y actualizan los datos cuando el cliente vuelve a la pestaña. Para React suelen ser TanStack Query y SWR. No están en el sandbox y el curso no tiene lecciones sobre ellas. Por dentro hacen lo mismo que aquí: una petición en un efecto, estados, una cancelación.

Una bandera en lugar de una cancelación. En lugar de un AbortController, la limpieza puede levantar una bandera: let ignore = false; en el efecto, ignore = true en la limpieza y if (!ignore) antes de asignar los productos. La petición antigua llega igualmente al servidor, pero su respuesta se descarta. Una cancelación detiene la propia petición. Los dos caminos resuelven la carrera.

StrictMode. En desarrollo con <StrictMode>, el efecto se ejecuta, se limpia y se ejecuta otra vez. Por eso la red muestra dos peticiones, la primera cancelada. Es la prueba de que la limpieza funciona.

Ejercicio

Intentar de nuevo

Aquí la primera petición de los lácteos recibe un 500. El catálogo muestra «Los productos no se cargaron.», pero no hay forma de intentarlo de nuevo. Añade un botón «Intentar de nuevo» que vuelva a ejecutar la carga. La cancelación al cambiar rápido tiene que seguir. La comprobación espera el error, pulsa «Intentar de nuevo», espera «Cargando…» y 5 productos, y luego cambia rápido a la fruta y a las bebidas.

React

Test

Ponte a prueba

1. El cliente pulsa «Frutas y verduras» y luego «Bebidas». Las respuestas llegan en orden inverso. ¿Qué muestra el código sin limpieza?
2. ¿Por qué la petición está en una async function load() dentro del efecto y no en useEffect(async () => …)?
3. ¿Por qué catch omite AbortError en lugar de mostrar «Los productos no se cargaron.»?
4. ¿Cómo vuelve a enviar la petición «Intentar de nuevo»?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·