Lección 08

¿Por qué el usuario pierde su trabajo al recargar?

Una caché de datos es una copia con fecha. El problema no es guardarla: es saber cuándo deja de ser verdad, qué sobrevive a una recarga y qué pasa con lo que el usuario escribió sin conexión.

01Qué es una caché de datos en el cliente

Una caché de datos en el cliente es una copia de lo que te dio el servidor, guardada con dos datos extra: la clave (qué consulta representa) y la fecha de captura. No es la caché HTTP del navegador, que va por URL y cabeceras; esta la controla tu aplicación y vive en memoria, localStorage o IndexedDB (la base de datos del navegador).

Un dato es fresh mientras está dentro de la tolerancia que elegiste y stale cuando la pasó. Esa tolerancia es de producto: el precio de una acción está viejo a los 5 segundos; el nombre de un usuario aguanta 10 minutos. Con stale-while-revalidate pintas la copia igual y pides la nueva en segundo plano; cuando llega, la reemplazas sin spinner.

El dato viejo sigue visible mientras corre la revalidación La UI muestra el total de las 09:14 1.842 · 09:14 09:14 09:15 stale sirve la copia revalidate pide en segundo plano fresh reemplaza sin parpadeo
Entre stale y fresh la pantalla no se vacía: se ve la copia vieja mientras el fetch corre por detrás.

02Por qué sucede: invalidar, recargar y perder

El problema real no es llenar la caché, es vaciarla. Después de crear un pedido, la lista cacheada sigue diciendo 19. Tres estrategias: por tiempo (refetch cada N segundos; simple, pero muestra datos viejos y gasta peticiones), por evento (tras una mutación marcas las claves afectadas; preciso, exige saber qué toca cada escritura) y por versión (el servidor devuelve un ETag y el cliente descarta si cambió; robusto, necesita soporte del servidor).

Lo que debe sobrevivir a una recarga no es estado de React: es estado de la URL. Filtros, página, pestaña y búsqueda en searchParams: recargar reconstruye la misma vista, el botón atrás funciona y el enlace se comparte. Lo que no va ahí: tokens, textos largos y borradores. Un borrador se guarda aparte, con una versión, y se recupera al volver.

Una actualización optimista aplica el cambio en la copia local antes de que el servidor conteste, guarda el valor anterior y lo restaura si la petición falla. Se siente instantánea porque no espera la red. El riesgo: muestras una promesa como si fuera un hecho; si el servidor valida distinto, el valor salta hacia atrás. Por eso hay que avisar del fallo y reconciliar con la respuesta del servidor.

La escritura optimista y su reversión lo que ve el usuario toca Guardar pinta "nuevo" sigue en "nuevo" vuelve a "viejo" lo que pasa en el servidor PATCH /jobs/7 500 a los 400 ms no guardó nada revierte 0 ms ~16 ms 400 ms sin la copia anterior, el valor nuevo se queda y el usuario cree que guardó
El valor optimista dura lo que dura la petición; la reversión es la mitad del patrón.

Sin conexión, lo que permite que la app abra es el service worker: un script que el navegador instala aparte e intercepta la red aun con la pestaña cerrada. En install precarga el shell (HTML, JS y CSS con hash), en activate borra cachés viejas y en fetch decide: cache-first para archivos con hash, network-first para HTML y datos, y nunca cachear POST ni respuestas con datos personales.

Lo que el usuario escribe sin conexión va a una cola de escritura pendiente (IndexedDB) y se reenvía al volver. Ahí aparecen dos problemas: el orden (un renombre y luego un borrado deben reenviarse en ese orden) y los conflictos (el registro cambió en otro dispositivo). Se resuelve con última escritura gana (simple, pierde datos), merge por campo (necesita la versión base) o un 409 con la versión actual. Casi siempre decide el servidor: ve a todos los escritores y el orden canónico.

La cola offline conserva el orden y se reenvía al volver sin conexión: la cola vive en el dispositivo cola en IndexedDB 1 · renombrar 2 · borrar sobrevive a la recarga servidor aplica 1 → 2 en ese orden clave: una vez sin red: espera al volver, replay en orden
La cola no se pierde con la pestaña: queda en IndexedDB con su orden. Al reconectar se reenvía tal cual, y la clave de idempotencia evita que un reintento aplique dos veces.

03Cómo se ataca: guardar, aplicar y revertir

// Optimista con reversión: guarda, aplica, revierte.
async function guardarTitulo(id: string, titulo: string) {
  const clave = ['job', id];
  // 1. copia de seguridad
  const anterior = cache.get(clave);

  // 2. la UI ya lo muestra
  cache.set(clave, { ...anterior, titulo });
  marcarPendiente(id, true);

  try {
    const ruta = '/jobs/' + id;
    const guardado = await api.patch(ruta, { titulo });
    // 3. manda el servidor
    cache.set(clave, guardado);
  } catch (error) {
    // 4. revierte
    cache.set(clave, anterior);
    avisar('No se guardó el título. Intenta de nuevo.');
    throw error;
  } finally {
    marcarPendiente(id, false);
  }
}

React trae el mismo patrón en useOptimistic: la copia optimista vive mientras la acción está pendiente y se descarta al terminar; si la acción lanza, vuelve al valor real.

04Cómo diagnosticarlo

HerramientaQué se miraCómo se ve el síntoma
Application › Service Workers Estado y versión del SW. "waiting" sirviendo HTML viejo; "redundant": falló la instalación.
Application › Cache Storage Entradas por caché y versión. HTML viejo, dos versiones a la vez, datos privados cacheados.
Network › Offline Qué falla y qué sale de caché. Todo rojo salvo el shell: abre pero no lee.
Consola Errores 409/412 y "Failed to fetch". Conflicto sin manejar; fetch sin catch.
Network, contar peticiones Peticiones duplicadas por vista. Dos GET iguales: falta deduplicación. Dos PATCH: doble envío.
Application › IndexedDB Cola pendiente: tamaño, orden e ids. Crece y no baja: el reenvío falla en silencio.

Si al recargar aparece el dato viejo y luego salta, falla la invalidación; si no aparece nada, el service worker no sirve el shell.

Para decirlo en la entrevista

“We cache data by query key with a freshness window and serve stale data while revalidating.”

“Optimistic updates keep the previous value so we can roll back and tell the user if the write fails.”

“Offline writes go into an ordered queue with an idempotency key, and the server resolves conflicts.”

Comprueba que lo tienes

Cuatro preguntas. Al comprobar se marca la lección como completada.

La UI muestra el título nuevo y el PATCH falla a los 400 ms. ¿Qué hace la reversión?

Dos pestañas editan el mismo registro. ¿Por qué el conflicto se resuelve en el servidor?

Tras crear un pedido, el listado cacheado sigue en 19. ¿Qué lo corrige con menos riesgo?

Un usuario renombra sin conexión y al volver la escritura se reenvía dos veces. ¿Qué falta?