Módulo 1: The Four Kinds Of State

El estado del servidor es un caché

Descripción

Esta es la gaveta que da sentido a toda la guía, y la que casi todo el mundo clasifica mal. El estado del servidor son los datos cuya verdad vive en el backend: los productos de Mercado, el perfil del usuario, el historial de pedidos, las reseñas. La frase que hay que grabar es esta: no es tu estado, es un caché. Tú, en el frontend, nunca tienes "los productos"; tienes una copia de los productos que te dio el servidor la última vez que preguntaste. El dueño de la verdad es el backend; tú administras una copia en caché de esa verdad, y esa copia tiene propiedades que ninguna de las otras tres cajas tiene: se pone vieja (stale), hay que volver a pedirla (revalidar), se comparte entre todos los usuarios, y la petición puede fallar o estar cargando. Confundir esa copia con estado propio —tratarla como si fuera un useState tuyo— es el error más común y más caro del frontend, y le dedicamos la lección 6 entera. Aquí instalamos la idea y la medimos.

Conexión con el módulo. La regla de decisión del módulo pregunta, primero de todo, "¿la verdad vive en el backend?". Ahora entiendes por qué está en primer lugar y no en el último: porque es la trampa más cara. Los products los usan muchos componentes —parecerían global (lección 3)—, pero su verdad no es del cliente: es del backend. En cuanto una pieza responde "sí" a esa primera pregunta, se va directo a esta caja, sin pasar por global ni por local, sin importar cuántos la usen. Y una vez aquí, cambia todo: no la guardas ni la sincronizas a mano; la tratas como un caché que se revalida. La herramienta que hace eso bien —React Query / TanStack Query— es de los módulos 4, 5 y 6 de esta guía; aquí solo reconocemos la caja y entendemos, con código ejecutado, por qué necesita reglas propias.

Una analogía: tu dinero, en el banco

Vuelve a la casa, pero ahora sal de ella. Tu saldo no vive en tu casa ni en tu bolsillo: vive en el banco. Lo que tú ves cuando abres la app del banco es una foto de tu saldo —lo que decía en el momento en que la app preguntó—. El banco es el dueño de la verdad; tú tienes una copia.

Y esa copia tiene una propiedad que ninguna cosa de tu casa tiene: se pone vieja sola, sin que tú toques nada. Imagina que abres la app a las 9:00 y dice "$1,000". A las 9:05, un amigo te transfiere $200. Tu pantalla sigue diciendo $1,000 —no porque esté rota, sino porque muestra la foto de las 9:00—. La verdad en el banco ya es $1,200, pero tu copia quedó desactualizada (stale). ¿Cómo te enteras? Solo hay una forma: volver a preguntarle al banco (bajar a refrescar la app, que dispara una nueva consulta). En ese momento tu copia se pone al día. A eso se le llama revalidar: reconocer que tu foto puede estar vieja y pedir una nueva.

Dos matices más de la imagen. Tu saldo lo ven también el cajero, el banquero, el sistema de la tarjeta —es una verdad compartida entre muchos, no privada tuya—; por eso puede cambiar "por debajo" (otro proceso la modifica). Y a veces, cuando quieres consultar el saldo, el banco no responde —la red se cae, el servidor está ocupado—: la consulta puede fallar o quedarse cargando. Ninguna de esas cosas —ponerse vieja, compartirse, poder fallar— le pasa a tu cepillo (local) ni al control remoto (global): son exclusivas de tener una copia de la verdad de otro. Eso es el estado del servidor. Los products de Mercado son tu saldo del banco: una copia de algo que vive afuera y que se pone vieja en cuanto el backend cambia.

Las cuatro propiedades que hacen distinto al estado del servidor

Antes del código, fijemos qué lo separa de las otras cajas. Estas cuatro propiedades son la razón por la que necesita herramientas propias:

propiedad          que significa                         que NO tiene el estado local/global
─────────────────  ───────────────────────────────────  ──────────────────────────────────
se pone STALE      tu copia envejece cuando el backend   local/global no envejecen solos:
                   cambia por debajo                     su verdad es tuya, no de afuera
hay que REVALIDAR  para saber la verdad hay que volver   local/global ya son la verdad; no
                   a preguntarle al servidor             hay a quien "volver a preguntar"
se COMPARTE        muchos usuarios (y procesos) ven y     tu estado de cliente es solo tuyo,
                   mutan la misma verdad remota          en tu sesion
puede FALLAR /     la peticion puede tardar (loading)    asignar un useState nunca "falla"
  estar CARGANDO   o romperse (error)                    ni "carga"; es instantaneo y local

Cuando ves estas cuatro propiedades juntas, sabes que estás ante estado del servidor —y que useState solo no alcanza para manejarlo—. Un useState no sabe que su valor envejeció, no sabe volver a pedirlo, no tiene un estado de "cargando" ni de "error". Por eso, tratar datos remotos con useState te obliga a reconstruir todo eso a mano (con useEffect, banderas de carga, manejo de errores, sincronización)... que es exactamente lo que React Query hace por ti. Pero antes de la herramienta, hay que ver el problema. Vamos a medirlo.

Ejemplo trabajado: la copia manual queda stale; el caché se refresca

Modelamos en Node un backend que es la fuente de la verdad (tiene los productos y su precio), y dos formas de tener esos productos en el cliente:

  • A) Copia manual. Como harías con useState: pides los productos una vez, los guardas, y los tratas como tuyos. Nunca vuelves a preguntar.
  • B) Caché. Reconoces que es una copia de datos remotos: la marcas como stale cuando sabes que el backend cambió, y revalidas (vuelves a pedir) cuando hace falta.

En medio, un admin aplica una oferta: baja el precio del mouse en el backend. Vemos qué muestra cada uno después:

// Estado del SERVIDOR: no es tuyo, es un CACHE de datos que viven en el backend.
// La verdad esta en el backend y puede cambiar por debajo (otro usuario, un admin).
// Comparamos dos formas de tener los "products" en el cliente:
//   A) copia manual con useState  -> se queda STALE cuando el backend cambia
//   B) un cache que sabe refetchar -> vuelve a estar FRESH

// --- El backend: la fuente de la verdad ---
const backend = {
  products: [
    { id: 'p1', name: 'Wireless Mouse', priceCents: 2599 },
    { id: 'p2', name: 'Mechanical Keyboard', priceCents: 8900 },
  ],
  fetchProducts() {
    // devuelve una FOTO del estado actual del backend (una copia)
    return this.products.map((p) => ({ ...p }));
  },
  // un admin baja el precio del mouse (oferta): la verdad cambio en el backend
  applySale() { this.products[0].priceCents = 1999; },
};

const priceOf = (list, id) => '$' + (list.find((p) => p.id === id).priceCents / 100).toFixed(2);

console.log('=== El estado del servidor es un cache ===\n');

// --- A) Copia manual: useState(products) + copiar una sola vez, tratarlo como propio ---
let naiveProducts = backend.fetchProducts(); // copie al montar y me olvide
console.log('A) Copia manual (useState, la trato como MIA):');
console.log('   al montar:  Wireless Mouse = ' + priceOf(naiveProducts, 'p1'));

// --- B) Cache que reconoce que es una copia de datos remotos ---
function makeCache(fetchFn) {
  let data = null, isStale = true;
  return {
    read() { return data; },
    markStale() { isStale = true; }, // "lo que tengo ya no es de fiar"
    revalidate() { if (isStale) { data = fetchFn(); isStale = false; } },
  };
}
const productsCache = makeCache(() => backend.fetchProducts());
productsCache.revalidate();
console.log('\nB) Cache (reconozco que es una COPIA del backend):');
console.log('   al montar:  Wireless Mouse = ' + priceOf(productsCache.read(), 'p1'));

// --- La verdad cambia en el backend ---
console.log('\n>>> Un admin aplica una oferta: el precio del mouse baja a $19.99 en el BACKEND.\n');
backend.applySale();
productsCache.markStale(); // el cache se entera de que su copia quedo vieja

// --- Que ve cada uno ahora ---
console.log('A) La copia manual NO se entero (nunca volvio a pedir):');
console.log('   ahora:      Wireless Mouse = ' + priceOf(naiveProducts, 'p1') + '   <- STALE (el backend dice $19.99)');

productsCache.revalidate(); // el cache vuelve a pedir porque estaba stale
console.log('\nB) El cache revalida (vuelve a pedir al backend):');
console.log('   ahora:      Wireless Mouse = ' + priceOf(productsCache.read(), 'p1') + '   <- FRESH (coincide con el backend)');

console.log('\nVerdad en el backend:  Wireless Mouse = ' + priceOf(backend.fetchProducts(), 'p1'));

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

=== El estado del servidor es un cache ===

A) Copia manual (useState, la trato como MIA):
   al montar:  Wireless Mouse = $25.99

B) Cache (reconozco que es una COPIA del backend):
   al montar:  Wireless Mouse = $25.99

>>> Un admin aplica una oferta: el precio del mouse baja a $19.99 en el BACKEND.

A) La copia manual NO se entero (nunca volvio a pedir):
   ahora:      Wireless Mouse = $25.99   <- STALE (el backend dice $19.99)

B) El cache revalida (vuelve a pedir al backend):
   ahora:      Wireless Mouse = $19.99   <- FRESH (coincide con el backend)

Verdad en el backend:  Wireless Mouse = $19.99

Esta salida es la tesis del módulo en cuatro líneas. Léela con calma:

Al montar, las dos formas se ven idénticas. Tanto la copia manual como el caché muestran $25.99. Ese es el engaño: cuando el backend no ha cambiado, tratar los datos como propios parece funcionar perfecto. Por eso el error sobrevive tanto tiempo sin que nadie lo note —en desarrollo, con datos que no cambian, todo se ve bien—.

Entonces la verdad cambia en el backend. El admin aplica la oferta: el mouse pasa a $19.99 en el backend. Este es el momento clave, y es lo que hace especial al estado del servidor: la verdad cambió por debajo, sin que el frontend hiciera nada. Nadie tocó tu app; el dato de otro se movió.

La copia manual queda STALE. Como nunca volvió a preguntar, la copia manual sigue diciendo $25.99 —está vieja—. El usuario ve un precio que ya no existe. Este es el bug: el frontend muestra con total confianza un dato que el backend ya cambió. Y fíjate en que no hay ningún error visible —no crashea, no tira excepción—; simplemente miente en silencio. Los peores bugs de estado del servidor son así: callados.

El caché se refresca. El caché, en cambio, sabía que su copia podía envejecer: cuando se enteró del cambio se marcó stale, y al revalidar volvió a preguntarle al backend, obteniendo $19.99fresh, coincide con la verdad—. Esa es toda la diferencia entre tratar el dato como propio (se queda viejo) y tratarlo como caché (se refresca). El caché no evita que la copia envejezca —eso es inevitable, la verdad es de otro—; lo que hace es saber que envejeció y volver a pedir.

La moraleja no es "el caché es más código" (aquí lo es, un poco). Es que el estado del servidor exige ese comportamiento —marcar stale, revalidar— porque su verdad no es tuya. Si lo tratas como useState, tarde o temprano muestras $25.99 cuando el mundo dice $19.99. React Query existe para darte ese comportamiento sin que lo escribas a mano; pero primero tenías que ver por qué hace falta.

La regla "¿de quién es la verdad?" en su forma más pura

Toda la lección cabe en la pregunta madre del módulo, aplicada a datos remotos:

¿de quien es la verdad de los "products"?

  del BACKEND. Tu frontend tiene una COPIA.
       │
       ├─ la copia se pone STALE cuando el backend cambia
       ├─ para saber la verdad, hay que REVALIDAR (volver a pedir)
       ├─ la verdad se COMPARTE (otros usuarios/procesos la mutan)
       └─ pedirla puede FALLAR o estar CARGANDO
       │
       └─> caja: SERVER  (se maneja como un cache, no como useState)

Si la verdad es del backend, la pieza es server —punto—, y hay que administrarla como lo que es: una copia que se revalida. Da igual que muchos componentes la usen (eso tienta a clasificarla como global) o que solo uno la muestre (eso tienta a clasificarla como local): la procedencia de la verdad manda sobre el número de consumidores. Esta es la razón exacta por la que la regla de decisión del módulo pregunta por el backend antes que por cualquier otra cosa.

Errores comunes

Tratar la respuesta de la red como un useState normal. Qué pasa: const [products, setProducts] = useState([]) y un useEffect que hace fetch una vez y llama a setProducts. Por qué pasa: es lo único que react-fundamentals enseñó, y "funciona" en la demo. Cómo detectarlo: la copia nunca se revalida; cuando el backend cambia, el usuario ve datos viejos sin ningún error visible (como la copia manual del ejemplo, clavada en $25.99). Cómo corregirlo: reconoce que ese dato es un caché de una verdad remota, clasifícalo como server, y déjaselo a una herramienta que sabe revalidar (React Query, M5). El useState no sabe que su valor envejeció.

Pensar que "cargado una vez" es "correcto para siempre". Qué pasa: se asume que, como los productos se pidieron al montar, ya están bien hasta que el usuario recargue la página. Por qué pasa: se olvida que la verdad es compartida y cambia por debajo (otro usuario compra el último en stock, un admin cambia un precio). Cómo detectarlo: bugs de "el usuario vio disponible algo que ya no lo estaba", precios desactualizados, contadores que no cuadran. Cómo corregirlo: acepta que la copia envejece y necesita revalidarse —en ciertos momentos (al volver a la pestaña, tras una mutación, cada cierto tiempo)—. Un caché de verdad tiene una política de frescura; una copia manual no tiene ninguna.

Ignorar los estados de carga y error. Qué pasa: se maneja solo el caso feliz (los datos llegaron), sin contemplar "cargando" ni "falló". Por qué pasa: al tratar el dato como un useState local, uno no piensa en que pedirlo lleva tiempo y puede romperse. Cómo detectarlo: pantallas en blanco mientras carga, o que crashean cuando la red falla, porque el código asume que los datos "ya están". Cómo corregirlo: el estado del servidor siempre tiene tres caras —cargando, datos, error—, porque hablar con el backend es asíncrono y falible. Modelarlas es parte de manejar esta caja (y React Query las da listas: isLoading, isError, data).

Ejercicios

Ejercicio 1 — ¿Server o no? Para cada pieza, decide si es estado del servidor (su verdad vive en el backend) y nombra al menos una de las cuatro propiedades que lo delata: (a) la lista de productos del catálogo; (b) el tema claro/oscuro que el usuario eligió; (c) el número de unidades en stock de un producto; (d) si el menú de una tarjeta está abierto; (e) las reseñas de un producto; (f) los items que el usuario metió en su carrito (antes del checkout).

Ver solución
  • (a) catálogo de productos → server. Verdad del backend. Se pone stale (entran productos nuevos, cambian precios); se comparte (todos los usuarios ven el mismo catálogo).
  • (b) tema → NO server (es global de cliente). La verdad es del cliente (el usuario lo eligió), no del backend. No se pone stale por algo remoto. Global (lección 3).
  • (c) stock de un producto → server. El caso más claro de por qué importa: el stock cambia por debajo (otros usuarios compran). Se pone stale rapidísimo; se comparte. Mostrar un stock viejo vende algo que ya no existe.
  • (d) menú abierto → NO server (es local). Verdad de un solo componente, no del backend. Local (lección 2).
  • (e) reseñas → server. Verdad del backend; se pone stale (llegan reseñas nuevas); se comparte; pedirla puede fallar o cargar.
  • (f) items del carrito antes del checkout → NO server (es global de cliente). Sutil: mientras el carrito vive en el cliente (aún no se mandó al backend), su verdad es del cliente → global. En el momento en que se confirma el pedido, lo que el backend guarda (la orden) sí es server. La misma "compra" cruza dos cajas según dónde viva su verdad en cada momento.

Ejercicio 2 — Por qué la copia manual mintió. Con el ejemplo ejecutado, explica por qué la copia manual siguió mostrando $25.99 después de la oferta, mientras el caché mostró $19.99. ¿Por qué es especialmente peligroso que este bug no produzca ningún error visible?

Ver solución

La copia manual pidió los productos una vez, al montar, y los guardó como si fueran suyos. Cuando el admin bajó el precio en el backend, esa copia no se enteró —nunca volvió a preguntar—, así que se quedó con la foto vieja: $25.99. El caché, en cambio, reconoció que su copia podía envejecer: se marcó stale al saber del cambio y revalidó (volvió a pedir), obteniendo $19.99, que coincide con la verdad del backend.

Es peligroso precisamente porque no crashea. Un error que rompe la pantalla se nota y se arregla; pero mostrar $25.99 cuando el precio real es $19.99 se ve perfectamente normal —la app funciona, no hay excepción, no hay pantalla roja—, y sin embargo está mintiendo. El usuario compra confiando en un dato viejo. Estos bugs silenciosos de datos stale son los más difíciles de detectar porque no gritan; solo están mal. Por eso el estado del servidor necesita revalidación como política, no como algo que uno "recuerda hacer".

Ejercicio 3 — Diseña la política de frescura. El caché del ejemplo solo revalidó cuando lo marcamos stale a mano. En una app real, ¿en qué momentos concretos tendría sentido revalidar los products de Mercado para que la copia no mienta? Da al menos tres, y explica qué cambio del backend cubre cada uno.

Ver solución

Tres momentos razonables para revalidar el catálogo (o el detalle de un producto):

  1. Cuando el usuario vuelve a la pestaña (regresa al navegador tras estar en otra app). Cubre cambios que ocurrieron mientras no miraba: precios que cambiaron, productos agotados. Es el "bajo a refrescar la app del banco al volver".
  2. Después de una acción que muta el backend (por ejemplo, tras comprar, revalidar el stock; tras dejar una reseña, revalidar las reseñas). Cubre el cambio que tú mismo provocaste, para que la UI refleje el nuevo estado. (Esto es exactamente el ciclo write → invalidate → refetch del módulo 6.)
  3. Cada cierto tiempo (revalidación periódica), útil para datos que cambian seguido y que el usuario mira mucho rato —un stock en una venta flash, por ejemplo—. Cubre cambios continuos hechos por otros usuarios.

La idea de fondo: la copia siempre puede envejecer, así que "cuándo revalidar" es una decisión de diseño (una política de frescura), no algo que se hace una vez. React Query trae estas políticas listas (staleTime, revalidar al enfocar la ventana, invalidar tras mutaciones); manejarlas es lo que aprenderás en M4-M6. Reconocer que hacen falta es esta lección.

Resumen y siguiente paso

En esta lección abriste la gaveta central de la guía: el estado del servidor, y clavaste su definición: no es tu estado, es un caché de datos cuya verdad vive en el backend. Viste sus cuatro propiedades exclusivas —se pone stale, hay que revalidar, se comparte, puede fallar/cargar— y por qué ninguna la tienen el estado local ni el global. Lo mediste ejecutando el bug rey del módulo: una copia manual (estilo useState) que se quedó en $25.99 cuando el backend bajó el precio a $19.99 —mintiendo en silencio—, contra un caché que revalidó y volvió a estar fresh. Y entendiste, con la analogía del banco, por qué la regla "¿de quién es la verdad?" pone el backend primero: es la clasificación más cara de equivocar.

Antes de avanzar deberías poder: definir el estado del servidor como un caché y nombrar sus cuatro propiedades; explicar por qué tratarlo como useState deja datos stale sin error visible; distinguirlo del estado global (verdad del backend vs. verdad del cliente, no "¿cuántos lo usan?"); y justificar por qué necesita una política de revalidación. La herramienta (React Query) es de los módulos 4-6; aquí clasificaste la caja y viste por qué es distinta.

La lección 5 abre la última gaveta: el estado de la URL, lo compartible y bookmarkeable —la búsqueda, los filtros, la página—. Ahí verás, ejecutado, cómo ?q=mouse&sort=price es la fuente de la verdad de esos datos, con un round-trip exacto entre estado y searchParams. Y después, la lección 6 vuelve sobre lo que viste aquí para medir la tesis completa: cómo el estado del servidor mal clasificado produce copias que divergen —el bug del módulo en toda su fuerza—.

Recursos