Rollback en fallo
Si el servidor rechaza la mutación, la predicción optimista se vuelve una mentira que hay que retirar: eso es el rollback, revertir la interfaz al estado anterior. Esta lección demuestra que el rollback depende por completo de un artefacto capturado antes de mentir —el snapshot del estado previo— y que sin ese snapshot la reversión es imposible. Detalla cómo capturar el estado en el momento exacto de aplicar la predicción, cómo restaurarlo de forma atómica y completa cuando la petición falla, y por qué hay que cancelar las revalidaciones en vuelo antes de tomar el snapshot para que una respuesta rezagada no corrompa ni el estado optimista ni la reversión. La tesis: el optimismo sin rollback no es audacia, es imprudencia; el snapshot es lo que lo vuelve ético.
Todo el edificio del optimismo descansa sobre una apuesta: que la mutación tendrá éxito. La mayoría de las veces gana, pero de vez en cuando el servidor rechaza la operación —falla la red, caduca la sesión, una regla de negocio dice que no— y en ese instante la predicción que pintaste se convierte en una mentira visible en la pantalla. El rollback es el acto de retirar esa mentira: devolver la interfaz al estado exacto que tenía antes de que te adelantaras. Y aquí surge la verdad incómoda de esta técnica: no puedes revertir a un pasado que no guardaste. El rollback no es un botón mágico de deshacer, sino la restauración de una fotografía que tuviste la precaución de tomar antes de mentir. Sin esa fotografía —el snapshot— el optimismo es un salto sin red; con ella, es un salto calculado del que siempre puedes volver.
- Comprender el rollback como la restauración del estado previo cuando el servidor rechaza la mutación.
- Capturar el snapshot del estado en el momento exacto de aplicar la predicción optimista.
- Restaurar ese snapshot de forma atómica y completa, sin dejar estados corruptos a medias.
- Cancelar las revalidaciones en vuelo antes del snapshot para evitar que una respuesta rezagada corrompa la reversión.
El snapshot: guardar el pasado antes de mentir
La secuencia correcta invierte el instinto. Antes de aplicar la predicción, captura el estado actual y guárdalo aparte; ese valor es tu única vía de regreso. Solo después mutas de forma optimista. Si la petición falla, restauras el snapshot; si tiene éxito, lo descartas. La captura debe ocurrir en el mismo instante que la mutación, no antes ni después, porque el snapshot tiene que reflejar exactamente el estado sobre el que se aplicó la predicción.
async function alternarMeGusta() {
const anterior = meGusta; // snapshot: el pasado, guardado
setMeGusta(!anterior); // prediccion optimista aplicada
try {
await api.alternarMeGusta(postId);
} catch {
setMeGusta(anterior); // rollback: restaura el snapshot
mostrarError("No se pudo guardar");
}
}
En un valor escalar como este, el snapshot es trivial. Pero cuando el estado optimista es una lista, un objeto anidado o una entrada de cache normalizada, el snapshot debe ser una copia fiel y completa de esa estructura antes de tocarla, porque restaurar solo una parte deja el resto viviendo en el futuro que nunca ocurrió.
// Snapshot de una estructura: una copia, nunca la misma referencia
const anterior = structuredClone(tareas); // fotografia independiente
setTareas((prev) =>
prev.map((t) => (t.id === id ? { ...t, hecha: !t.hecha } : t)),
);
// ...si la peticion falla: setTareas(anterior);
El error silencioso más común es guardar la referencia en vez del valor. Si haces const anterior = tareas y luego mutas ese mismo objeto, tu snapshot cambia con él: has fotografiado un espejo, no una escena. En estado inmutable el problema no aparece porque cada cambio produce un objeto nuevo y la referencia vieja sobrevive intacta; por eso la inmutabilidad y el rollback son aliados naturales. Si trabajas con estructuras mutables, clona de verdad —structuredClone o una copia profunda— antes de tocar nada.
Revertir de forma atómica y completa
Una reversión a medias es peor que no revertir: deja el estado en una mezcla incoherente de pasado y futuro fallido. Si tu mutación optimista tocó tres campos, el rollback debe restaurar los tres; si añadió un elemento y actualizó un contador, debe deshacer ambos. La regla es que el rollback restaure el snapshot entero de una vez, como una transacción que se aborta y vuelve al punto de guardado, no como una serie de correcciones campo a campo que podrían quedarse a mitad.
// Rollback atomico: un solo reemplazo, vuelve entero al pasado
onError(_error, _variables, ctx) {
setEstado(ctx.snapshot);
}
// Anti-patron: deshacer a mano cada efecto, con riesgo de quedar a mitad
onError() {
setContador((c) => c - 1); // y si esto corre
setLista((l) => l.slice(0, -1)); // pero esto lanza? estado corrupto
}
Restaurar el snapshot completo es, además, más simple de razonar: no necesitas recordar qué tocaste ni en qué orden deshacerlo, solo volver a la fotografía. El anti-patrón de deshacer efecto por efecto obliga a mantener sincronizada la lista de deshacer con la de hacer, y esa sincronización se rompe en cuanto alguien añade un cuarto efecto y olvida su reverso.
Un rollback técnicamente correcto pero mudo es una trampa de usabilidad. El usuario vio su acción surtir efecto y, un segundo después, la ve desvanecerse sin explicación: concluirá que la aplicación tiene fallos o, peor, no se dará cuenta y creerá que su cambio se guardó. Todo rollback debe ir acompañado de una señal clara —un aviso, un reintento ofrecido— que le diga qué pasó. La mecánica de la reversión y su comunicación son inseparables; la lección cinco desarrolla la parte de experiencia, pero retén ya que revertir en silencio es la mitad de un error.
flowchart TD S[capturar snapshot del estado] --> A[aplicar prediccion optimista] A --> R[disparar peticion] R --> OK[servidor confirma] R --> ERR[servidor rechaza] OK --> D[descartar snapshot] ERR --> B[restaurar snapshot completo] B --> N[avisar al usuario] style ERR fill:#f38ba8,color:#11111b style B fill:#fab387,color:#11111b style OK fill:#a6e3a1,color:#11111b
Carreras: cuando el pasado se mueve bajo tus pies
Hay una sutileza que causa los bugs más difíciles del optimismo. Imagina que, en el momento de mutar, hay una revalidación en vuelo —una petición previa que aún no ha respondido— cuyo resultado va a escribirse en el mismo estado. Si tomas el snapshot ahora y esa revalidación responde después, sobrescribirá tu estado optimista con datos viejos, o peor, contaminará tu snapshot con un valor que no corresponde al pasado real. La consecuencia es un rollback que restaura un estado equivocado. La defensa es cancelar las peticiones en vuelo sobre ese dato antes de capturar el snapshot, de modo que ninguna respuesta rezagada pueda colarse entre tu predicción y tu reversión.
La secuencia robusta tiene cuatro pasos previos a mutar y dos de desenlace. Primero, cancelar las revalidaciones en vuelo sobre el dato. Segundo, leer y guardar el snapshot. Tercero, aplicar la predicción optimista. Cuarto, disparar la petición. Y al desenlace: si falla, restaurar el snapshot y avisar; en todo caso, éxito o fallo, reconciliar con el servidor. Este orden no es casual: cada paso previene una carrera concreta, y saltarse el primero es la causa número uno de reversiones que restauran basura.
Escrito como código, ese orden canónico reúne las tres lecciones del nivel en una sola función: cancelar y capturar preparan el terreno, la predicción se aplica, y el desenlace bifurca entre rollback y reconciliación sin dejar ningún camino sin cubrir.
async function mutarSeguro(nuevo) {
await cancelarEnVuelo(["dato"]); // 1. cancelar carreras
const snapshot = leer(["dato"]); // 2. capturar el pasado
escribir(["dato"], nuevo); // 3. prediccion optimista
try {
await api.guardar(nuevo); // 4. disparar la peticion
} catch {
escribir(["dato"], snapshot); // rollback: restaura el snapshot
avisar("No se pudo guardar");
} finally {
invalidar(["dato"]); // reconciliar, pase lo que pase
}
}
Reconoce en esta plantilla el esqueleto de lo que la lección cuatro verá encarnado en onMutate, onError y onSettled: no es más que este mismo orden repartido en tres retrollamadas con nombre.
Si dos mutaciones optimistas coexisten sobre datos relacionados, una sola variable de snapshot compartida se pisaría a sí misma y un rollback restauraría el pasado equivocado. Encapsula el snapshot por invocación —una variable local en cada llamada, o el objeto de contexto que devuelve onMutate en TanStack Query— para que cada reversión disponga exactamente del pasado que le corresponde y no del que dejó su vecina. El snapshot no es un singleton global: es una propiedad de cada mutación individual.
Encapsular el snapshot por operación puede ser tan simple como un mapa indexado por el identificador de cada mutación en vuelo:
// Un snapshot por mutacion en vuelo, indexado por su id
const snapshots = new Map<string, Estado>();
function alMutar(mutId: string, actual: Estado) {
snapshots.set(mutId, structuredClone(actual)); // su propio pasado
}
function alFallar(mutId: string) {
restaurar(snapshots.get(mutId)!); // exactamente el pasado que le toca
snapshots.delete(mutId);
}
Es tentador ver el rollback como un mecanismo de recuperación de errores, una red de seguridad opcional que añades cuando tienes tiempo. Esa lectura invierte la relación real. El rollback no es un complemento del optimismo: es su condición de posibilidad moral. Adelantarte a mostrar un resultado no confirmado solo es defendible si has garantizado de antemano que puedes deshacerlo sin dejar rastro cuando te equivoques, y esa garantía tiene un nombre y un coste: el snapshot que capturaste antes de mentir. Mira lo que esto revela sobre la estructura del tiempo en tu aplicación. El optimismo te obliga a mantener dos versiones del estado coexistiendo: el pasado congelado en el snapshot y el futuro apostado en la predicción. Durante la ventana de incertidumbre, tu interfaz vive en un tiempo condicional —muestra un futuro que solo se hará real si el servidor coopera— mientras conserva intacto el pasado por si el futuro se cae. Los sistemas de bases de datos llevan medio siglo formalizando exactamente esto con la propiedad de atomicidad: una transacción o se aplica entera o se revierte entera al último punto de guardado, sin estados intermedios visibles. El rollback optimista es esa misma atomicidad trasladada a la percepción del usuario: la predicción se comporta como una transacción especulativa que, si aborta, no debe dejar ni una migaja del futuro que imaginaste. Por eso un optimismo sin rollback no es simplemente incompleto, es deshonesto: reclama la responsividad de adelantarse sin asumir la responsabilidad de poder retractarse. La madurez de un ingeniero de interfaces se mide, en buena parte, por si captura el snapshot antes de mutar por reflejo o solo después de que un bug se lo enseñe a la fuerza.
- Toma una mutación optimista y localiza dónde deberías capturar el snapshot; si no capturas ninguno, tu reversión ya está rota aunque no lo hayas notado.
- Fuerza un fallo del servidor de forma determinista y comprueba que la interfaz vuelve exactamente al estado previo, sin campos a medias.
- Convierte un rollback parcial —que solo restaura un campo de varios que tocaste— y observa el estado corrupto que deja; luego arréglalo restaurando el snapshot completo.
- Simula una revalidación en vuelo que responde justo después de tu mutación y verifica cómo contamina el estado si no la cancelaste antes del snapshot.
- Añade la cancelación de peticiones en vuelo como primer paso y confirma que el rollback ahora restaura el pasado correcto en lugar de basura rezagada.
- Comprueba si tu snapshot guarda una copia o una referencia: muta el estado tras capturarlo y verifica si el snapshot cambió con él; si cambió, tu rollback restauraría el presente, no el pasado.