Módulo 4: Server State Is Different
Las cuatro propiedades del estado del servidor
Descripción
Ya sabes que el estado del servidor no es tuyo: es un caché de una verdad que vive en el backend. Esta lección enumera las cuatro propiedades que se desprenden de ese hecho y que ninguna otra caja de estado tiene. Son el "kit" completo que hace que el estado del servidor necesite herramienta propia, y conviene tenerlas como una lista mental, porque cada una explica una pieza de React Query que verás en M5-M6. La copia del servidor se pone stale (envejece cuando el backend cambia por debajo), se comparte (otros usuarios y procesos mutan la misma verdad remota), es asíncrona (pedirla lleva tiempo: pasa por un estado de carga), y puede fallar (la red o el servidor se rompen: hay que manejar un estado de error). Las cuatro salen de una sola raíz —la verdad es de otro y hay que ir a buscarla por la red— y las cuatro las vamos a medir, no a afirmar.
Ninguna de estas cuatro propiedades le pasa al estado de cliente, y ese contraste es la mejor forma de reconocerlas. El tema no envejece solo, nadie más lo mueve, cambiarlo no "carga" y no puede "fallar". El carrito (mientras vive en el cliente) tampoco. Cuando ves una pieza de estado que tiene estas cuatro marcas juntas, sabes con certeza que estás ante estado del servidor —y que un useState no alcanza para manejarlo, porque un useState no sabe que su valor envejeció, no sabe volver a pedirlo, y no tiene un estado de "cargando" ni de "error"—.
Conexión con el módulo. La lección 2 fijó la naturaleza (no es tuyo, es un caché); esta enumera las consecuencias de esa naturaleza. Es la lección "de propiedades" que da vocabulario preciso para el resto del módulo: cuando la lección 5 hable de "la copia queda stale" o la lección 6 de "el estado loading/error", ya sabrás exactamente qué son y de dónde salen. Cada propiedad, además, anticipa una función de React Query (M5-M6): "stale" → staleTime; "revalidar" → refetch y invalidate; "compartido" → una caché por queryKey; "asíncrono/falla" → isLoading/isError. Aquí las nombramos y medimos; allá se implementan.
Una analogía: el clima que consultas
Cambiemos de imagen para fijar bien las propiedades. Piensa en el clima. Cuando abres una app del clima y ves "23°C, soleado", estás viendo una copia de un dato cuya verdad no controlas: el clima real lo produce la atmósfera, y un servicio meteorológico lo mide y lo publica. Tú solo consultas. Y esa consulta tiene, exactas, las cuatro propiedades del estado del servidor.
Se pone stale: el clima cambia solo, sin avisarte. Consultas a las 8:00 y dice "soleado"; a las 11:00 está lloviendo, pero tu pantalla —si no la refrescaste— sigue diciendo "soleado". Tu copia envejeció porque la verdad se movió. Hay que revalidar: la única forma de saber el clima actual es volver a consultar (bajar a refrescar la app). Se comparte: el clima no es tuyo; lo consultan millones de personas a la vez, y la verdad la mueve algo externo (la atmósfera), no tú. Es asíncrono y puede fallar: cuando pides el clima, la app tarda un momento en traerlo (un spinner: cargando), y a veces el servicio no responde —sin señal, servidor caído— y ves un error con un botón de "reintentar".
Compáralo con la temperatura que tú pones en tu termostato: esa verdad es tuya, no cambia sola, nadie más la mueve, y ajustarla es instantáneo y no "falla". El clima es estado del servidor; el termostato es estado de cliente. Las cuatro propiedades son la diferencia entre consultar algo que otro controla y decidir algo que controlas tú. Guarda la imagen del clima: cada vez que dudes si un dato es del servidor, pregúntate si se parece más al clima (lo consultas, cambia solo, puede fallar) o al termostato (lo pones tú, es tuyo).
Ejemplo trabajado: las cuatro propiedades, una por una
Vamos a ejecutar las cuatro. Modelamos en Node un backend con un producto (la fuente de la verdad) y disparamos cada propiedad con un mini-escenario. Para las dos primeras (stale y compartido) veremos cómo la copia queda atrás cuando la verdad se mueve; para las dos últimas (asíncrono y falla) modelamos la máquina de estados de una petición (idle → loading → success o → error), que es la forma real en que una petición se vive en la UI. Antes, cómo se ve esa máquina en React —el estado que react-fundamentals te hacía manejar a mano—:
// La maquina de estados que TODA peticion vive: idle -> loading -> success | error.
function useProduct(id) {
const [status, setStatus] = useState('loading'); // arranca cargando
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/products/' + id)
.then((r) => r.json())
.then((d) => { setData(d); setStatus('success'); })
.catch((e) => { setError(e.message); setStatus('error'); });
}, [id]);
return { status, data, error }; // la UI decide que pintar segun status
}
Fíjate en que esa máquina —tres piezas de estado, un useEffect, un .then y un .catch— existe solo porque el dato es asíncrono y falible. El estado de cliente no necesita nada de esto. Ahora ejecutemos las cuatro propiedades:
// Las cuatro propiedades que hacen distinto al estado del servidor, cada una MEDIDA.
// backend = fuente de la verdad; el cliente tiene una copia en cache.
const backend = {
product: { id: 'p1', name: 'Wireless Mouse', priceCents: 2599, stock: 3 },
fetch() { return { ...this.product }; },
};
const money = (c) => '$' + (c / 100).toFixed(2);
console.log('=== Las 4 propiedades del estado del servidor ===\n');
// ---------- 1) SE PONE STALE: la verdad se mueve, tu copia envejece ----------
console.log('1) STALE — tu copia envejece cuando el backend cambia por debajo');
let copy = backend.fetch();
console.log(' copia al montar: ' + money(copy.priceCents));
backend.product.priceCents = 1999; // el backend cambia
console.log(' backend ahora: ' + money(backend.fetch().priceCents));
console.log(' tu copia (sin refetch): ' + money(copy.priceCents) + ' <- STALE\n');
// ---------- 2) SE COMPARTE: otros usuarios mutan la misma verdad ----------
console.log('2) COMPARTIDO — otros usuarios mueven la misma verdad remota');
copy = backend.fetch();
console.log(' tu copia: stock = ' + copy.stock);
backend.product.stock -= 3; // otros usuarios compran las 3 unidades
console.log(' otro usuario compra 3 -> backend: stock = ' + backend.fetch().stock);
console.log(' tu copia (sin refetch): stock = ' + copy.stock + ' <- muestras "disponible" algo agotado\n');
// ---------- 3) ES ASINCRONO: pedirlo lleva tiempo -> loading ----------
// Modelamos la maquina de estados de una peticion: idle -> loading -> success
console.log('3) ASINCRONO — pedirlo no es instantaneo: pasa por "loading"');
let req = { status: 'idle', data: null, error: null };
console.log(' estado: ' + req.status);
req = { status: 'loading', data: null, error: null }; // se dispara el fetch
console.log(' estado: ' + req.status + ' <- la UI muestra un spinner');
req = { status: 'success', data: backend.fetch(), error: null }; // llega la respuesta
console.log(' estado: ' + req.status + ' -> data.name = "' + req.data.name + '"\n');
// ---------- 4) PUEDE FALLAR: la red se cae ----------
console.log('4) PUEDE FALLAR — la red o el servidor pueden romperse: "error"');
let req2 = { status: 'loading', data: null, error: null };
console.log(' estado: ' + req2.status);
req2 = { status: 'error', data: null, error: 'NetworkError: fetch failed' };
console.log(' estado: ' + req2.status + ' -> error = "' + req2.error + '"');
console.log(' la UI debe mostrar "Reintentar", no crashear.\n');
console.log('=== Ninguna de las 4 le pasa al estado de cliente ===');
console.log(' El theme no se pone stale, no lo mueve otro, no carga, no falla: su verdad es tuya.');
console.log(' Estas 4 propiedades son la razon de que el servidor necesite una herramienta propia.');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Las 4 propiedades del estado del servidor ===
1) STALE — tu copia envejece cuando el backend cambia por debajo
copia al montar: $25.99
backend ahora: $19.99
tu copia (sin refetch): $25.99 <- STALE
2) COMPARTIDO — otros usuarios mueven la misma verdad remota
tu copia: stock = 3
otro usuario compra 3 -> backend: stock = 0
tu copia (sin refetch): stock = 3 <- muestras "disponible" algo agotado
3) ASINCRONO — pedirlo no es instantaneo: pasa por "loading"
estado: idle
estado: loading <- la UI muestra un spinner
estado: success -> data.name = "Wireless Mouse"
4) PUEDE FALLAR — la red o el servidor pueden romperse: "error"
estado: loading
estado: error -> error = "NetworkError: fetch failed"
la UI debe mostrar "Reintentar", no crashear.
=== Ninguna de las 4 le pasa al estado de cliente ===
El theme no se pone stale, no lo mueve otro, no carga, no falla: su verdad es tuya.
Estas 4 propiedades son la razon de que el servidor necesite una herramienta propia.
Repasa las cuatro con la analogía del clima:
1) Stale. Tu copia dijo $25.99 al montar; el backend bajó el precio a $19.99; tu copia, sin volver a pedir, se quedó en $25.99. Es el clima que consultaste a las 8:00 y sigue diciendo "soleado" cuando ya llueve. La copia no está rota; está vieja, porque la verdad se movió y ella no se enteró.
2) Compartido. Tu copia decía stock = 3; otros usuarios compraron las tres unidades y el backend pasó a stock = 0; tu copia siguió en 3. Este es el más peligroso: estás mostrando "disponible" algo que ya se agotó. La verdad es compartida —muchos la mutan a la vez—, así que envejece rapidísimo y sin que tú hagas nada. Nadie de tu app tocó el stock; otro lo movió.
3) Asíncrono. La petición pasó por idle → loading → success. El loading no es un adorno: es un estado real por el que la UI tiene que pasar, porque pedir a la red lleva tiempo. Mientras carga, muestras un spinner; cuando llega, muestras los datos. El estado de cliente no tiene este paso —setState es instantáneo, no hay "cargando"—.
4) Puede fallar. La petición pasó por loading → error. La red se cae, el servidor devuelve un 500, el usuario está sin señal: la petición falla, y tu UI tiene que manejarlo —mostrar "Reintentar", no una pantalla en blanco ni un crash—. Asignar un useState nunca "falla"; pedir a un backend, sí. Por eso el estado del servidor siempre tiene tres caras posibles —cargando, datos, error—, y modelarlas es parte de manejarlo.
El cierre lo dice: ninguna de las cuatro le pasa al theme. Ese es el test rápido. Si un dato se pone stale, lo comparten y mutan otros, carga y puede fallar, es del servidor. Si nada de eso le pasa —cambia solo cuando tú decides, al instante, sin fallar—, es de cliente.
Cómo cada propiedad se convierte en una pieza de React Query
No es casualidad que sean cuatro propiedades: cada una tiene una respuesta concreta en la herramienta que verás en M5-M6. Guarda este mapa; es el puente entre "el problema" (este módulo) y "la solución" (los siguientes):
propiedad del estado del servidor que le da React Query (M5-M6)
────────────────────────────────── ──────────────────────────────────────────
se pone STALE staleTime: cuanto tiempo la copia se considera fresca
hay que REVALIDAR refetch automatico (al enfocar, al reconectar) + invalidate
se COMPARTE una cache por queryKey: una copia para todos
es ASINCRONO (loading) isLoading / isPending: estado de carga hecho
puede FALLAR (error) isError / error + reintentos: estado de error hecho
Léelo al revés y verás por qué React Query se ve como se ve: no es una librería arbitraria con mil opciones, es la respuesta punto por punto a las cuatro propiedades del estado del servidor. staleTime existe porque las copias envejecen. La caché por queryKey existe porque la verdad se comparte. isLoading e isError existen porque pedir es asíncrono y falible. Cuando llegues allá, reconocerás cada pieza porque ya sufriste la propiedad que la justifica.
Errores comunes
Manejar solo el "caso feliz" (ignorar loading y error). Qué pasa: se escribe el componente asumiendo que los datos "ya están", sin contemplar "cargando" ni "falló". Por qué pasa: al tratar la copia como un useState de cliente, uno olvida que pedirla lleva tiempo y puede romperse. Cómo detectarlo: pantallas en blanco mientras carga, o que crashean (Cannot read property 'name' of null) cuando el fetch aún no volvió o falló. Cómo corregirlo: el estado del servidor siempre tiene tres caras —cargando, datos, error—, porque es asíncrono y falible. Modela las tres. (React Query las da hechas: isLoading, isError, data.)
Creer que "recién cargado" es "correcto para siempre". Qué pasa: como los datos llegaron al montar, se asume que quedan bien hasta que el usuario recargue. Por qué pasa: se olvida que la verdad es compartida y cambia por debajo. Cómo detectarlo: bugs de "vi disponible algo que ya no estaba", precios viejos, contadores desfasados —el caso del stock = 3 que ya era 0—. Cómo corregirlo: la copia envejece porque otros mutan la verdad; necesita una política de revalidación (volver a pedir en ciertos momentos), no un solo fetch. Es el clima: hay que re-consultar.
Confundir "carga" con "error" (o tratarlos como el mismo caso). Qué pasa: se muestra el mismo mensaje genérico para "aún no llega" y para "falló", o se deja al usuario sin forma de reintentar tras un error. Por qué pasa: los dos "no muestran datos", así que se colapsan en uno. Cómo detectarlo: un spinner infinito cuando en realidad la petición falló, o un "algo salió mal" cuando en realidad solo está cargando. Cómo corregirlo: son estados distintos —loading es transitorio y se resuelve solo; error es terminal y necesita una acción (reintentar)—. La máquina idle → loading → success | error los separa a propósito.
Ejercicios
Ejercicio 1 — ¿Clima o termostato? Para cada dato, di si tiene las cuatro propiedades del estado del servidor (parecido al clima) o ninguna (parecido al termostato), y nombra al menos una propiedad que lo delate: (a) el número de likes de una publicación; (b) el volumen del reproductor de música que el usuario ajustó; (c) el precio de una acción en la bolsa; (d) si el usuario tiene abierto el menú lateral.
Ver solución
- (a) likes → clima (servidor). Se pone stale (otros dan like ahora mismo); se comparte (la verdad es del backend); pedirlo carga y puede fallar. Envejece rapidísimo.
- (b) volumen → termostato (cliente, local). La verdad es del usuario, en su app; no se pone stale, nadie más lo mueve, ajustarlo es instantáneo y no falla.
- (c) precio de una acción → clima (servidor), extremo. Cambia cada segundo (stale casi al instante); compartido por todos; asíncrono y falible. El caso más agresivo de "hay que revalidar seguido".
- (d) menú lateral abierto → termostato (cliente, local). Verdad de un solo componente; no tiene ninguna de las cuatro propiedades.
Ejercicio 2 — Por qué el stock es el más peligroso. En la corrida, tu copia mostró stock = 3 cuando el backend ya decía 0. Explica qué propiedad causó esto y por qué este caso concreto puede costar dinero o confianza, más que un precio viejo.
Ver solución
Lo causó la propiedad compartido: el stock es una verdad remota que otros usuarios mutan todo el tiempo (cada compra lo baja). Tu copia se hizo al montar (3), y mientras tanto tres personas compraron las tres unidades, dejando el backend en 0 —pero tu copia no se enteró—.
Es más caro que un precio viejo por lo que provoca río abajo: si muestras "disponible" algo agotado, el usuario lo agrega al carrito, llega al checkout, y ahí falla la compra (o peor, se confirma y no hay producto que enviar). Un precio viejo se corrige mostrando el correcto; un stock viejo genera pedidos imposibles de cumplir, carritos frustrados y desconfianza. Por eso el stock suele necesitar la revalidación más agresiva de toda la app —se re-consulta muy seguido, casi como el precio de una acción—.
Ejercicio 3 — Diseña la máquina de estados. Un componente muestra el perfil del usuario, que viene de GET /me. Enumera los estados por los que puede pasar la petición y, para cada uno, qué debería pintar la UI. Luego di por qué el estado de cliente (el tema) no necesita esta máquina.
Ver solución
Los estados de GET /me, en orden posible:
loading(oidle → loading): la petición está en curso. La UI pinta un spinner o un esqueleto del perfil. No hay datos aún, así que no debe intentar leeruser.name.success: llegaron los datos. La UI pinta el perfil (user.name, avatar, etc.).error: la petición falló (sin red, 500, token vencido). La UI pinta un mensaje claro con un botón de "Reintentar", nunca una pantalla en blanco ni un crash.
El tema no necesita esta máquina porque su verdad es tuya y síncrona: cambiarlo es un setState que ocurre al instante, en memoria. No hay un "mientras llega" (no viaja por la red) ni un "falló" (asignar una variable no puede fallar). Por eso el estado de cliente se maneja con un solo valor, y el del servidor con toda una máquina de tres estados. Esa máquina es exactamente lo que React Query te da hecha (isLoading, isError, data) en vez de que la escribas en cada componente —lo verás repetido a mano en la lección 6—.
Resumen y siguiente paso
En esta lección enumeraste y mediste las cuatro propiedades que hacen distinto al estado del servidor: se pone stale (tu copia envejece cuando el backend cambia —el precio que pasó a $19.99 y tu copia siguió en $25.99—), se comparte (otros mutan la verdad —el stock que ya era 0 mientras tú mostrabas 3—), es asíncrono (pasa por loading —un spinner mientras llega—) y puede fallar (pasa por error —"Reintentar", no un crash—). Las anclaste con el clima —lo consultas, cambia solo, lo comparten todos, y a veces el servicio no responde— frente al termostato —tu verdad, instantánea, tuya—. Y viste el mapa que convierte cada propiedad en una pieza de React Query: staleTime, la caché por queryKey, isLoading, isError.
Antes de avanzar deberías poder: nombrar las cuatro propiedades y reconocer cuáles delatan a una pieza como del servidor; distinguir loading de error como estados separados; y explicar por qué el estado de cliente no necesita la máquina idle → loading → success | error.
La lección 4 empieza a medir los males del fetching manual a escala, con los dos primeros: sin caché y sin dedup. Vas a ver, ejecutado, cómo tres componentes que muestran los mismos productos hacen tres peticiones al mismo endpoint (porque cada useEffect pide por su cuenta), y cómo dos componentes que montan a la vez piden el mismo dato dos veces —y cómo una capa indexada por queryKey baja esos números a uno—. Ahí la propiedad "se comparte" que viste aquí se vuelve un problema concreto de peticiones duplicadas.
Recursos
- TanStack Query, "Important Defaults" — tanstack.com/query/latest/docs/framework/react/guides/important-defaults. Qué significan
staley la revalidación en la práctica, y por defecto: los conceptos (stale, refetch) que ejecutamos a mano aquí. En inglés. - TkDodo, "Why You Want React Query" — tkdodo.eu/blog/why-you-want-react-query. El recuento de todo lo que el estado del servidor exige (loading, error, stale, revalidación) y que tendrías que manejar tú a mano. En inglés.
- React, "You Might Not Need an Effect" (sección "Fetching data") — react.dev/learn/you-might-not-need-an-effect#fetching-data. La máquina
loading/error/dataescrita a mano conuseEffect, y por qué es frágil. En inglés. - TanStack Query, "Queries" — tanstack.com/query/latest/docs/framework/react/guides/queries. Los estados de una query (
pending,error,success) —la máquina de esta lección, hecha herramienta—. En inglés.