Módulo 6: Effects And Side Effects

Fetch de datos con un efecto

Descripción

Llegamos al efecto que más vas a escribir al principio de tu vida con React: pedir datos a un servidor. Los productos de Mercado no están en el código; viven en la red. Al abrir la app, el App debe pedirlos y, cuando lleguen, mostrarlos. Ese "pedir datos" es un side effect de manual: la red es un sistema externo, la petición ocurre fuera de React, y el componente debe sincronizarse con la respuesta cuando llegue. El patrón crudo tiene tres piezas: un estado que guarda los datos (arranca en null, que significa "cargando"), un useEffect que hace la petición, y un setState que se llama cuando la respuesta llega —y que dispara el re-render con los datos ya puestos—. Esta lección construye ese patrón, lo ejecuta con una red simulada determinista, y deja planteadas las trampas que la lección 6 resuelve.

Conexión con el módulo. Aquí confluye todo lo anterior: el efecto sincroniza con un sistema externo (la red, lección 2), corre al montar con [] (lección 3), y —lo veremos en la 6— necesita cleanup para no sufrir race conditions (lección 4). Es también donde se hace más visible la frontera de la guía: el fetch en el cliente con useEffect es lo de aquí; el fetch en el servidor (SSR) es Next.js, y las librerías de datos (React Query, SWR) son la guía de estado y datos. Esas librerías existen para no escribir a mano lo que verás aquí; primero hay que entender el patrón crudo.

Una analogía: pedir algo por correo

Pedir datos a un servidor es como pedir algo por correo. No es instantáneo: mandas la solicitud (el pedido), sigues con tu vida, y en algún momento —quizás pronto, quizás tarde— llega el paquete. Tres cosas caracterizan el proceso, y las tres tienen su reflejo en el patrón:

  • Mientras esperas, no tienes el paquete. Tu casa está en estado "esperando". En React, eso es products = null: todavía no llegaron los datos, y la UI muestra Loading....
  • Mandar el pedido es una acción hacia afuera. Sales de tu casa (de React) y le hablas al correo (la red). Eso va en un efecto.
  • Cuando el paquete llega, actualizas tu casa. Lo desempacas y lo pones en su lugar. En React, eso es setProducts(data): guardas lo que llegó, y la UI se re-renderiza para mostrarlo.

Lo importante de la analogía: entre "mandar el pedido" y "recibir el paquete" pasa tiempo, y durante ese tiempo tu app sigue viva y respondiendo. Por eso el fetch no puede ir en el render (que es instantáneo y puro): va en un efecto, que manda el pedido y agenda qué hacer cuando llegue la respuesta. Guarda la imagen —pedido, espera, llegada—, porque las trampas de la lección 6 salen justo de que el tiempo de espera de dos pedidos puede cruzarse.

Ejemplo trabajado: cargar los productos al montar

Construyamos la carga de productos del storefront. El App (aquí, un ProductList para simplificar) arranca con products = null (cargando), y al montarse dispara el efecto que pide los datos. El componente como lo escribirás en React:

function ProductList() {
  const [products, setProducts] = useState(null); // null = cargando

  useEffect(() => {
    fetchProducts().then((data) => setProducts(data)); // pide; al llegar, guarda
  }, []); // [] = pedir una sola vez, al montar

  if (products === null) return <p>Loading...</p>;
  return (
    <ul>
      {products.map((p) => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Léelo con la analogía: fetchProducts() es mandar el pedido (una acción hacia la red); .then((data) => setProducts(data)) es qué hacer cuando llegue el paquete (guardarlo en estado). El array [] dice "pide una sola vez, al montar" —no en cada render, que bombardearía al servidor—. Y mientras products es null, la UI muestra Loading...; cuando llegan los datos, setProducts dispara el re-render y la lista aparece.

Ahora ejecutémoslo. No hay red, así que la simulamos con una cola manual determinista: fetchProducts no resuelve al instante ni con un setTimeout real (que sería impredecible), sino que encola la respuesta, y nosotros la "entregamos" cuando queremos con deliverNext(). Así el orden es 100% reproducible:

// "Red" simulada: una cola de respuestas que drenamos a mano (determinista).
const network = [];
const PRODUCTS = [
  { id: 'p1', name: 'Wireless Mouse', priceCents: 2599 },
  { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900 },
  { id: 'p3', name: 'USB-C Hub', priceCents: 3450 },
];
function fetchProducts(onResult) {
  console.log('  [effect]  fetchProducts() -> pedido enviado a la "red" (aun no llega)');
  network.push(() => onResult(PRODUCTS)); // la respuesta queda ENCOLADA, no llega aun
}
function deliverNext() {
  const respond = network.shift();
  console.log('  [red]     la respuesta llega -> se llama a setProducts(...)');
  respond();
}

let ui;
function ProductList() {
  const [products, setProducts] = useState(null); // null = "cargando"
  useEffect(() => {
    fetchProducts((data) => setProducts(data));
  }, []); // [] = pedir una sola vez, al montar
  const label = products === null ? 'Loading...' : `${products.length} productos`;
  console.log(`  [render]  <ProductList>: ${label}`);
  return { setProducts };
}

function renderAndCommit() { hookCursor = 0; stateCursor = 0; ui = ProductList(); commit(); }
rerender = renderAndCommit;

console.log('=== Fetch al montar: null (cargando) -> datos ===\n');
renderAndCommit();  // primer render: products=null, el effect dispara el fetch
deliverNext();      // la red responde -> setProducts -> re-render con la lista
console.log(`\nEstado final: ${stateCells[0].length} productos cargados.`);

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== Fetch al montar: null (cargando) -> datos ===

  [render]  <ProductList>: Loading...
  [effect]  fetchProducts() -> pedido enviado a la "red" (aun no llega)
  [red]     la respuesta llega -> se llama a setProducts(...)
  [render]  <ProductList>: 3 productos

Estado final: 3 productos cargados.

Lee las cuatro líneas como las cuatro etapas del pedido por correo.

[render] Loading...: el primer render corre con products = null (el estado inicial). La app no tiene los datos todavía, así que muestra Loading.... Esto es clave: la UI se pinta de inmediato, sin esperar a la red; el usuario ve algo (el "cargando") desde el primer instante.

[effect] fetchProducts() -> pedido enviado: después del commit (recuerda el orden render → commit → effect de la lección 2), corre el efecto y manda el pedido a la red. Pero fíjate en el texto: "aun no llega". El efecto no espera a la respuesta; solo la dispara y agenda qué hacer cuando llegue.

[red] la respuesta llega -> setProducts(...): en algún momento posterior (aquí, cuando llamamos a deliverNext()), la red responde. Se ejecuta el callback que agendamos: setProducts(PRODUCTS). El estado pasa de null a la lista de tres productos, y eso dispara un re-render.

[render] 3 productos: el re-render corre con products ya lleno, así que la UI muestra los 3 productos en vez del Loading.... El paquete llegó y la casa se actualizó.

El estado final es 3 productos cargados. Nota la forma del patrón: la app nunca se "congeló" esperando la red. Se pintó cargando, mandó el pedido, siguió viva, y cuando llegaron los datos se re-pintó con ellos. Ese es el ritmo del fetch en el cliente: render inmediato (cargando) → efecto que pide → respuesta → re-render con datos.

Profundización: el patrón completo, y por qué las librerías existen

Los tres estados de un fetch: cargando, datos, error. El ejemplo modeló dos (null = cargando, y la lista = datos), pero un fetch de verdad tiene tres desenlaces, y una UI honesta los maneja los tres:

function ProductList() {
  const [products, setProducts] = useState(null); // null = cargando
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchProducts()
      .then((data) => setProducts(data))
      .catch((err) => setError(err)); // la red puede fallar
  }, []);

  if (error) return <p>Something went wrong.</p>;
  if (products === null) return <p>Loading...</p>;
  return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

Tres ramas: error primero, luego cargando, luego los datos. Escribir esto a mano en cada componente que pide datos es repetitivo y fácil de equivocar —y todavía no incluye la race condition (lección 6), ni el reintento, ni la caché—.

Por qué el efecto es el lugar correcto (por ahora). La red es un sistema externo; pedir datos es un side effect; y "al montar el componente, pide sus datos" es sincronizar el componente con el servidor. Encaja perfecto con useEffect(..., []). El fetch no puede ir en el render (que es puro e instantáneo: dispararía una petición en cada render y no podría esperar la respuesta) ni en un event handler de "montaje" (no existe tal evento). El efecto es la herramienta que React da para "haz esto cuando el componente aparece".

Por qué existen React Query y SWR —y por qué no las usamos aquí—. Todo lo que acabas de ver —el estado de carga, el de error, y lo que viene en la lección 6 (la race condition, el doble efecto, la falta de caché, la re-petición al volver a la pestaña)— es trabajo repetitivo y propenso a bugs. Librerías como React Query y SWR existen justamente para encapsularlo: le pides los datos con una sola llamada (useQuery), y ellas manejan carga, error, caché, deduplicación de peticiones iguales, revalidación y race conditions por ti. En una app de producción, casi nunca escribirás el fetch crudo con useEffect: usarás una de esas librerías. Pero para entender qué hacen —y para saber cuándo useEffect sigue siendo lo correcto (sincronizar con sistemas que no son data-fetching: una conexión, un listener, un timer)— tienes que ver el patrón crudo primero. Eso es lo de este módulo. La API de las librerías, con su caché y sus mutaciones, es la guía de estado y datos (frontend-state-and-data).

La otra frontera: el fetch en el servidor (SSR). En este módulo pides datos desde el navegador, después de que la página cargó: por eso hay un momento de Loading.... En una app con Next.js, buena parte de los datos se piden en el servidor, antes de mandar el HTML al navegador —así la página llega ya con sus datos, sin useEffect y sin estado de carga—. Ese modelo (Server Components, data fetching en el servidor) es la guía de Next.js (nextjs-app-router). No compite con lo de aquí: son dos lugares distintos donde pueden vivir los datos, y cada guía cubre el suyo.

Errores comunes

Poner el fetch en el cuerpo del render. Qué pasa: se llama a fetchProducts() directamente en el cuerpo del componente, fuera de useEffect. Por qué pasa: parece lo directo. Cómo detectarlo: el servidor recibe una petición por cada render (docenas por segundo mientras el usuario interactúa); la app se traba. Cómo corregirlo: el fetch va en useEffect(..., []) para pedir una vez al montar. El render es puro e instantáneo; no es lugar para pedir datos.

Olvidar el estado de "cargando" (o inicializar mal). Qué pasa: se hace useState([]) y se mapea de una vez, sin distinguir "todavía no llegó" de "llegó vacío"; el usuario ve una lista vacía sin saber si está cargando o si de verdad no hay nada. Por qué pasa: parece más simple arrancar con []. Cómo detectarlo: no puedes mostrar Loading... porque no distingues cargando de vacío. Cómo corregirlo: arranca en null (o un flag isLoading), que significa "aún no hay respuesta", y muestra Loading... mientras sea null. Cuando llega la respuesta (aunque sea []), ya sabes que cargó.

Creer que setProducts es inmediato (leer el valor justo después). Qué pasa: se hace setProducts(data); console.log(products) esperando ver data, y sale el valor viejo. Por qué pasa: se olvida el snapshot del módulo 3 —setState agenda un re-render, no muta la variable actual—. Cómo detectarlo: el log inmediato muestra null o el valor anterior. Cómo corregirlo: el nuevo valor se ve en el próximo render, no en la línea siguiente. Si necesitas actuar sobre los datos recién llegados, hazlo dentro del .then, con la variable data, no leyendo el estado.

Ejercicios

Ejercicio 1 — Completa el fetch. Este componente pide un producto por su id, pero está incompleto. Complétalo con el estado de carga y el efecto que pide al montar:

function ProductDetail({ id }) {
  // ??? estado
  // ??? efecto
  // ??? render segun el estado
}
Ver solución
function ProductDetail({ id }) {
  const [product, setProduct] = useState(null); // null = cargando

  useEffect(() => {
    fetchProduct(id).then((data) => setProduct(data));
  }, [id]); // depende de id: re-pide si cambia el producto

  if (product === null) return <p>Loading...</p>;
  return <h1>{product.name}</h1>;
}

Tres piezas: (1) estado product que arranca en null (cargando); (2) useEffect que pide con fetchProduct(id) y guarda el resultado con setProduct —con [id] en el array, porque el efecto usa id y debe re-pedir si el id cambia (lección 3; poner [] aquí sería mentir)—; (3) render que muestra Loading... mientras product es null, y el nombre cuando llegó. (Este efecto, al depender de un id cambiante, necesitará el cleanup de la lección 6 para la race condition.)

Ejercicio 2 — Traza las etapas. Sin correr nada, escribe las cuatro líneas de salida (los [render], [effect], [red]) del ejemplo trabajado si PRODUCTS tuviera un solo producto en vez de tres.

Ver solución
  [render]  <ProductList>: Loading...
  [effect]  fetchProducts() -> pedido enviado a la "red" (aun no llega)
  [red]     la respuesta llega -> se llama a setProducts(...)
  [render]  <ProductList>: 1 productos

Las etapas son idénticas —render cargando, efecto que pide, respuesta, re-render con datos—; lo único que cambia es la última línea, que ahora muestra 1 productos porque la lista tiene un elemento. El patrón no depende de cuántos datos lleguen; depende de la secuencia null → pedir → respuesta → datos.

Ejercicio 3 — ¿Efecto, servidor o librería? Un compañero pregunta cómo cargar los productos "de la forma correcta". Explícale las tres opciones que este módulo distingue y cuándo usar cada una: (a) useEffect con fetch crudo; (b) React Query / SWR; (c) fetch en el servidor con Next.js. Di también dónde se aprende cada una.

Ver solución
  • (a) useEffect con fetch crudo (lo de este módulo): pides los datos desde el cliente, al montar, manejando null/datos/error a mano. Sirve para entender el mecanismo y para casos simples, pero deja a tu cargo la race condition, la caché y los reintentos. Se aprende aquí.
  • (b) React Query / SWR: una librería que encapsula todo lo de (a) —carga, error, caché, deduplicación, revalidación, race conditions— detrás de un useQuery. Es lo que usarás en producción para datos del servidor en el cliente. Se aprende en la guía de estado y datos (frontend-state-and-data).
  • (c) Fetch en el servidor con Next.js: pides los datos antes de mandar la página al navegador (Server Components), sin useEffect ni estado de carga. Ideal para el contenido inicial de la página. Se aprende en la guía de Next.js (nextjs-app-router).

La regla: para aprender el mecanismo, (a); para producción en el cliente, (b); para datos iniciales de la página, (c). Este módulo enseña (a) para que entiendas lo que (b) y (c) resuelven.

Resumen y siguiente paso

En esta lección construiste el efecto más pedido: el fetch de datos. El patrón crudo tiene tres piezas —un estado que arranca en null (cargando), un useEffect(..., []) que manda la petición al montar, y un setState que se llama cuando la respuesta llega y dispara el re-render con los datos—. Lo ejecutaste con una red simulada determinista y viste el ritmo exacto: [render] Loading...[effect] manda el pedido → [red] responde → [render] con los datos. Entendiste por qué el efecto es el lugar correcto (la red es externa; el render es puro e instantáneo) y por qué existen React Query / SWR (para no escribir a mano la carga, el error, la caché y las race conditions) —con la frontera clara: el fetch crudo es de aquí, las librerías son de la guía de estado y datos, y el fetch en el servidor es de Next.js—. Y guardaste la analogía del pedido por correo: mandas la solicitud, sigues con tu vida, y actualizas la casa cuando llega el paquete.

Antes de avanzar deberías poder: escribir un fetch al montar con su estado de carga; explicar por qué va en un efecto y no en el render; nombrar los tres estados (cargando, datos, error); y ubicar la frontera entre useEffect crudo, las librerías de datos, y el fetch en el servidor.

La lección 6 abre la trampa que este patrón esconde. Si dos pedidos por correo salen casi al mismo tiempo —porque el usuario escribió rápido y cada tecla disparó una búsqueda— pueden llegar fuera de orden, y la respuesta vieja pisar a la nueva: una race condition. Ahí verás cómo el cleanup de la lección 4, con una bandera ignore, descarta la respuesta obsoleta —y por qué StrictMode corre el efecto dos veces en desarrollo justo para destapar los efectos a los que les falta esa protección—.

Recursos