El servidor no es cliente: es una cache
El error que abre el nivel no es de sintaxis ni de rendimiento, sino de categoría: tratar los datos remotos como estado propio. Esta lección argumenta por qué el estado de servidor es una cache de una verdad ajena, distingue al dueño del lector, nombra las cuatro preguntas de cache (frescura, revalidación, deduplicación e invalidación) y explica por qué meterlo en el store global es el error conceptual que más código estropea en la web moderna.
Hay un error que no es de sintaxis ni de rendimiento, sino de categoría, y por eso sobrevive a cualquier refactor: meter los datos del servidor en el mismo cajón que el estado propio de tu aplicación. Un perfil, una lista de pedidos, el resultado de una búsqueda no son estado tuyo. Son la copia de una verdad que vive en una base de datos que tu cliente no gobierna, tomada en un instante y condenada a envejecer desde que la recibes. Lo que tienes en el navegador no es el dato: es una hipótesis sobre el dato. Y una hipótesis sobre algo ajeno no se guarda como se guarda lo propio: se cachea, con una política que diga cuándo dejar de creerla. Este nivel nace de tomarse esa distinción en serio y seguirla hasta sus consecuencias.
- Distinguir el estado de servidor del de cliente por su propiedad: quién es dueño de la verdad.
- Entender por qué una copia de datos remotos es una cache y no un store.
- Nombrar las cuatro preguntas de cache: frescura, revalidación, deduplicación e invalidación.
- Reconocer por qué meter el servidor en el store global es un error de categoría.
Quién es dueño de la verdad
La distinción entre estado de cliente y de servidor no es de tamaño ni de origen, sino de autoridad. Del estado de cliente eres el dueño: cuando escribes que el menú está abierto, el menú está abierto; tu escritura es la verdad por definición, porque nadie más puede contradecirla.
Del estado de servidor eres, en el mejor de los casos, un lector con licencia temporal. El original vive en otra máquina, y otro usuario, otro proceso o el propio backend pueden cambiarlo mientras tu copia duerme en memoria. Tu versión es cierta solo hasta que deja de serlo, y lo grave es que deja de serlo sin avisarte.
Piénsalo como una fotografía frente a un espejo. El estado de cliente es un espejo: refleja en vivo lo que de verdad hay, porque la fuente y el reflejo son lo mismo. El estado de servidor es una fotografía: fiel en el instante del disparo y cada vez menos fiel después, porque el mundo que retrató siguió moviéndose sin ella.
Esa diferencia de autoridad cambia las preguntas que tiene sentido hacerse. Sobre el estado de cliente preguntas cuál es el valor, y la respuesta es total y presente. Sobre el estado de servidor esa pregunta es ingenua; las útiles son cuándo lo pedí, si sigue siendo válido, si debería volver a preguntar. Son preguntas sobre el tiempo y la confianza, no sobre el valor. Y son, exactamente, las que resuelve una cache.
Una copia que envejece sola
Toda copia de una verdad ajena tiene una propiedad que el estado local jamás tiene: se vuelve obsoleta sin que toques nada. No hay bug que provoque la obsolescencia; es la condición natural de una copia mientras el original sigue vivo en otra parte.
Aceptar eso introduce una variable que el estado propio ignora por completo: el tiempo. Un valor local es atemporal, vale hasta que tú lo cambies; una copia remota lleva pegada una fecha implícita de captura, y su credibilidad decae desde ese instante. Gestionar estado de servidor es, en el fondo, gestionar esa decadencia.
Quien intenta cubrir esa gestión a mano, con useEffect y useState, no está manejando estado: está reescribiendo una cache, peor y con más bugs que las que ya existen. El fragmento de abajo parece correcto y, sin embargo, no resuelve casi nada de lo que una cache resuelve.
// Parece correcto; en realidad es una cache incompleta
function Perfil({ id }: { id: string }) {
const [data, setData] = useState<Usuario | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let vivo = true;
fetch(`/api/usuario/${id}`)
.then((r) => r.json())
.then((u) => vivo && setData(u))
.catch((e) => vivo && setError(e));
return () => { vivo = false; };
}, [id]);
// Sin frescura, sin revalidacion, sin deduplicacion, sin invalidacion,
// sin reintentos, y con una carrera si id cambia deprisa.
}
Ese useEffect es el retrato del problema: cada carencia del comentario final es una responsabilidad que una cache asume por diseño y que aquí queda sin cubrir. Y no son adornos; son justo las que fallan en producción, con datos viejos en pantalla y peticiones duplicadas que nadie pidió.
Las cuatro preguntas de una cache
Gobernar una copia remota es responder cuatro preguntas que un valor propio nunca plantea, y no son opcionales: si no las contestas tú, las contesta el azar, y el azar contesta mal.
La frescura pregunta cuánto tiempo confías en la copia antes de sospechar de ella, y se responde con el staleTime. La revalidación pregunta qué evento te obliga a volver a pedirla: recuperar el foco de la ventana, reconectar la red, el paso de un intervalo. La deduplicación pregunta cómo evitar que diez componentes pidiendo el mismo recurso disparen diez peticiones en vez de una. Y la invalidación pregunta, cuando tú mismo cambias el dato con una escritura, qué copias quedan mentirosas y hay que tirar.
Frescura
Toda copia envejece. El staleTime decide cuánto la crees fresca antes de considerarla vieja y candidata a refrescarse.
Revalidación
Al reenfocar la ventana, reconectar o pasado un intervalo, la cache vuelve a pedir el dato en segundo plano sin bloquear la vista.
Deduplicación
Diez componentes que piden la misma clave disparan una sola petición y comparten el resultado. La clave es la identidad del dato.
Invalidación
Cuando escribes y cambias la verdad remota, marcas las copias afectadas como obsoletas para que se vuelvan a pedir. Es la coherencia tras un cambio.
Las cuatro se enredan en cuanto la app crece. Dos pestañas abiertas, un usuario que edita en otra ventana, una lista que se ve en tres sitios a la vez: cada escenario activa una pregunta distinta, y ninguna se resuelve guardando el dato y ya. Por eso el problema no es de almacenamiento, sino de política sobre el tiempo.
El error de categoría: el servidor en el store global
Durante años el reflejo por defecto fue: llega un dato del servidor, lo guardo en el store global. Parece inocente, porque es un dato y el store guarda datos. Pero es un error de categoría, del mismo orden que archivar una fotografía en la carpeta de documentos editables: la foto retrata algo que ya pasó y no controlas, y editarla no cambia lo retratado.
Cuando metes datos remotos en el store de cliente, heredas la obligación de hacer a mano las cuatro cosas de arriba. El slice de abajo parece código de dominio y no lo es: es una cache casera, incompleta, enterrada entre tu estado real.
// El anti-patron: el servidor disfrazado de estado de cliente
const usuariosSlice = createSlice({
name: "usuarios",
initialState: { lista: [], cargando: false, error: null },
reducers: {
pedir: (s) => { s.cargando = true; },
llegaron: (s, a) => { s.cargando = false; s.lista = a.payload; },
fallo: (s, a) => { s.cargando = false; s.error = a.payload; },
},
});
// Y ahora TU mantienes a mano: caducidad, revalidacion al reenfocar,
// deduplicacion de peticiones e invalidacion tras cada escritura.
La medición del ecosistema lo confirma sin ambigüedad: buena parte de la caída de Redux entre 2020 y 2026 se explica porque una herramienta especializada, TanStack Query, se llevó justo esa responsabilidad y dejó el store para lo que de verdad es estado de cliente. No fue una derrota de prestaciones, sino de categorías: el servidor encontró un dueño mejor que el store.
flowchart LR DB[servidor: la verdad] -->|fetch| CA[cache: copia con politica] CA -->|render inmediato| V[vista] CA -.->|no es lo mismo| ST[store: estado propio] ST -->|render| V style DB fill:#f38ba8,color:#11111b style CA fill:#fab387,color:#11111b style ST fill:#a6e3a1,color:#11111b style V fill:#89b4fa,color:#11111b
Conviene un matiz para no volverse dogmático. Una respuesta remota que solo se usa una vez y se descarta —un token de pago de un solo uso, el resultado de un envío que no se vuelve a leer— no necesita la maquinaria de una cache; es un valor de paso. La distinción que importa no es de dónde vino el dato, sino si vas a seguir leyéndolo mientras su original puede cambiar. Si lo lees repetidamente y su dueño vive fuera, es cache de servidor con todas sus letras; si lo consumes y lo olvidas, trátalo como lo efímero que es.
Cuando el servidor vive en tu store global, los síntomas son siempre los mismos: pantallas que muestran datos viejos tras guardar, dos vistas del mismo recurso que discrepan, spinners que tapan datos perfectamente útiles, y decenas de líneas de sincronización que se rompen al tocar una pieza. Ninguno es un bug de lógica: todos son el precio de obligar a una cache a fingir que es una fuente de verdad. El día que separas las dos clases, esa familia entera de síntomas no se arregla, deja de ser posible.
El salto de madurez de esta disciplina ocurre el día en que dejas de preguntar qué valor tiene este dato y empiezas a preguntar qué política gobierna esta copia. Es un cambio de pregunta, y por eso es difícil: no se arregla con más código, sino con otra forma de mirar. El estado de servidor es verdad prestada, la tienes un rato, envejece en tus manos y hay que devolverla al día, y toda verdad prestada exige una política, no un simple valor. La política responde las cuatro preguntas que un valor nunca contesta: cuánto tiempo creerla, qué evento fuerza a refrescarla, qué copias mueren cuando tú mismo escribes, y qué muestras mientras esperas la confirmación del servidor. Ninguna de esas preguntas tiene sentido para el estado de cliente, porque del estado de cliente eres dueño y tu escritura es la verdad. Por eso mezclar las dos clases en el mismo cajón no es desorden, es un error de categoría: obligas a una cache a fingir que es una fuente de verdad, y pagas ese engaño en cada dato obsoleto que un usuario ve en pantalla, en cada reducer que reinventa mal la deduplicación, en cada sincronización manual que se rompe cuando cambias una pieza. Separa las dos clases por su propiedad, lo que posees frente a lo que solo lees, y descubrirás que más de la mitad de tus bugs de estado no eran bugs de lógica, sino de haber tratado como propio lo que era prestado.
- Abre tu store global y marca todo lo que en realidad llegó por HTTP: es servidor disfrazado de cliente.
- Para una de esas piezas, escribe sus cuatro preguntas de cache: frescura, revalidación, invalidación y qué muestras mientras esperas.
- Cuenta cuántas líneas de reducers, acciones o thunks existen solo para leer y sincronizar ese dato remoto.
- Argumenta, para un dato concreto, por qué guardarlo en el store obliga a reimplementar a mano lo que una cache hace por diseño.
- Distingue en tu app un dato remoto que sea cache de servidor de otro que sea solo un valor de paso, y justifica la diferencia.
- Reformula en voz alta la pregunta que cambia todo: no qué valor tiene, sino qué política gobierna esta copia.