En la práctica: onMutate y useOptimistic
Las tres fases teóricas del optimismo —aplicar, revertir, reconciliar— tienen dos encarnaciones canónicas en el ecosistema actual, y esta lección las pone frente a frente. El trío onMutate, onError y onSettled de TanStack Query mapea de forma exacta la disciplina de las lecciones previas: onMutate cancela, toma el snapshot y aplica la predicción; onError restaura ese snapshot; onSettled invalida para reconciliar. El hook useOptimistic de React 19 encarna la filosofía opuesta: un estado optimista efímero, superpuesto sobre una fuente de verdad, que se descarta solo cuando termina la acción asíncrona, sin rollback manual. La lección demuestra que ambas APIs no son alternativas intercambiables sino la codificación de dos modelos mentales: uno que recuerda y revierte a mano una edición durable en una cache compartida, otro que olvida por diseño una capa temporal sobre un estado que se reafirma solo.
Las tres lecciones previas construyeron el optimismo desde los principios: aplicar la predicción, revertirla si falla, reconciliarla con el servidor. Ahora bajamos al terreno donde esos principios se escriben de verdad, y descubrimos que el ecosistema ofrece dos encarnaciones muy distintas de la misma idea. TanStack Query la resuelve con un trío de retrollamadas —onMutate, onError, onSettled— que mapean uno a uno la disciplina que ya conoces. React 19, con su hook useOptimistic, la resuelve al revés: sin snapshot, sin rollback manual, con un estado optimista que existe solo mientras dura la acción asíncrona y se evapora cuando termina. No son dos sintaxis para lo mismo: son dos filosofías sobre qué es una predicción y cuánto debe durar.
- Mapear el trío
onMutate,onErroryonSettledde TanStack Query sobre las fases de aplicar, revertir y reconciliar. - Usar el contexto que devuelve
onMutatepara transportar el snapshot hastaonError. - Manejar
useOptimisticde React 19 como estado efímero ligado a una acción asíncrona. - Distinguir las dos filosofías: la edición durable en cache que se revierte a mano y la capa temporal que se descarta sola.
TanStack Query: el trío onMutate, onError, onSettled
En TanStack Query, una mutación optimista se configura con tres retrollamadas del useMutation, y cada una es una de las fases que ya estudiaste. onMutate se ejecuta antes de lanzar la petición: ahí cancelas las revalidaciones en vuelo, tomas el snapshot y aplicas la predicción a la cache; lo que devuelvas se convierte en el contexto que reciben las demás. onError recibe ese contexto y restaura el snapshot: es el rollback. onSettled corre al final pase lo que pase, con éxito o con fallo, e invalida la query para reconciliar con el servidor.
useMutation({
mutationFn: (texto: string) => api.crearTarea(texto),
async onMutate(texto) {
await qc.cancelQueries({ queryKey: ["tareas"] }); // cancelar
const anterior = qc.getQueryData(["tareas"]); // snapshot
qc.setQueryData(["tareas"], (v) => [...v, { id: "temporal", texto }]);
return { anterior }; // contexto -> onError
},
onError(_err, _texto, ctx) {
qc.setQueryData(["tareas"], ctx.anterior); // rollback
},
onSettled() {
qc.invalidateQueries({ queryKey: ["tareas"] }); // reconciliar
},
});
El diseño es una traducción literal de la teoría: onMutate es el orden canónico de la lección tres —cancelar, capturar, aplicar—; el valor que retorna viaja como context hasta onError para que el rollback tenga a mano el pasado; y onSettled garantiza la reconciliación sin importar el desenlace.
La pieza que hace elegante a este patrón es el objeto que onMutate devuelve. TanStack lo entrega intacto a onError y onSettled como tercer argumento. Ese conducto resuelve el problema de dónde vive el snapshot: no en una variable suelta del componente, expuesta a carreras entre mutaciones simultáneas, sino encapsulado por mutación, viajando con ella. Cada invocación lleva su propio pasado, y dos mutaciones concurrentes no pisan sus snapshots.
Podría parecer natural invalidar solo cuando la mutación tiene éxito, pero onSettled corre en ambos desenlaces por una razón profunda. Tras un fallo, tu estado local acaba de revertirse a un snapshot que puede haber envejecido mientras la petición viajaba; tras un éxito, la respuesta puede no reflejar efectos colaterales en otros datos. En los dos casos la única fuente fiable es el servidor, así que reconciliar pase lo que pase es más seguro que confiar en que el camino feliz o el de error dejaron el estado perfecto. Invalidar en onSettled es decir: haya ido bien o mal, vuelve a preguntar.
useOptimistic: el estado que se descarta solo
React 19 aborda el problema desde otra premisa. useOptimistic recibe el estado real y una función que describe cómo se ve con la predicción aplicada, y devuelve un valor optimista junto a una función para añadir predicciones. La clave está en su ciclo de vida: el valor optimista existe solo mientras una acción asíncrona —una transición— está pendiente. En cuanto la acción termina y el estado real se actualiza, React descarta la capa optimista y el valor real reaparece por sí solo. No escribes rollback: si la acción falla y el estado real no cambia, la predicción simplemente se desvanece y la base se reafirma.
function Comentarios({ comentarios, enviar }) {
const [optimistas, agregar] = useOptimistic(
comentarios,
(estado, nuevo) => [...estado, { texto: nuevo, pendiente: true }],
);
async function accion(formData) {
const texto = formData.get("texto");
agregar(texto); // capa optimista, visible durante la transicion
await enviar(texto); // al terminar, React descarta la capa
}
return (
<form action={accion}>
{optimistas.map((c, i) => (
<p key={i} style={{ opacity: c.pendiente ? 0.5 : 1 }}>{c.texto}</p>
))}
<input name="texto" />
</form>
);
}
Aquí no hay snapshot ni reversión explícita porque el modelo es otro: el estado optimista nunca fue durable. Era una superposición temporal sobre comentarios, la fuente de verdad, y al acabar la transición esa fuente vuelve a mandar sin que tengas que restaurarla a mano.
Conviene leer la firma del hook con cuidado, porque encierra su única restricción dura:
const [valorOptimista, agregarOptimista] = useOptimistic(estadoReal, reductor);
// agregarOptimista solo surte efecto dentro de una accion o de useTransition.
// Fuera de ese contexto, React no tiene ningun "pendiente" al que atar la
// capa optimista, y el cambio se ignora en la practica.
La magia del descarte automático tiene una contrapartida: useOptimistic no funciona en el vacío. Su capa solo existe mientras haya una transición pendiente que la sostenga, así que fuera de una acción de formulario o de un startTransition no tienes dónde colgar la predicción. Esto lo hace ideal para el optimismo ligado a una interacción concreta y lo descarta para el optimismo que debe persistir entre interacciones o compartirse entre componentes lejanos, terreno donde TanStack Query sigue siendo la respuesta.
Dos filosofías, un problema
Las dos APIs resuelven el mismo problema con modelos mentales opuestos, y elegir bien exige entender la diferencia.
TanStack Query
Centrado en la cache. La predicción es una edición durable a un estado compartido entre componentes. Tú tomas el snapshot, tú reviertes, tú invalidas. Control total y persistencia más allá de una sola acción.
useOptimistic
Centrado en el render. La predicción es una capa efímera ligada a una transición. React la descarta al terminar la acción. Cero rollback manual, pero el estado optimista no sobrevive fuera de esa acción.
Como regla, useOptimistic brilla cuando la predicción acompaña a una acción concreta y local —enviar un formulario, mandar un mensaje— y no necesita perdurar. TanStack Query brilla cuando la predicción edita datos de servidor cacheados que muchas vistas comparten y que deben reconciliarse con precisión. No compiten tanto como cubren tramos distintos del mismo espectro.
El contraste se ve mejor comparando sus dos ciclos de vida lado a lado: uno reparte la responsabilidad en tres retrollamadas que tú escribes; el otro la resuelve atando la predicción a la vida de una transición.
flowchart TD A[TanStack onMutate captura y predice] --> B[peticion viaja] B --> C[onError revierte a mano] B --> D[onSettled invalida y reconcilia] E[React agregar dentro de la accion] --> F[capa visible mientras pendiente] F --> G[la accion termina y la capa se descarta sola] style C fill:#fab387,color:#11111b style D fill:#89b4fa,color:#11111b style G fill:#a6e3a1,color:#11111b
Lo más instructivo de comparar estas dos herramientas no es su sintaxis sino la metafísica que cada una asume sobre la predicción. TanStack Query trata el valor optimista como una edición real a una memoria compartida y persistente: la cache. Por eso te obliga a la ceremonia completa —capturar el pasado, revertir a mano, reconciliar explícitamente— exactamente igual que un sistema transaccional que escribe en un almacén durable y debe poder abortar. En ese modelo la predicción tiene peso: modifica un estado que otros componentes leen, sobrevive a la acción que la originó, y por eso su reversión no puede ser automática, tienes que recordarla. useOptimistic asume lo contrario: la predicción es una proyección efímera, una capa de presentación que se dibuja encima de una fuente de verdad inmutable durante el intervalo exacto de una transición. No hace falta revertir lo que nunca se guardó; basta con dejar de dibujarlo. React puede permitirse este olvido por diseño porque ata la vida de la predicción a la vida de la acción asíncrona, de modo que el final de la acción es, automáticamente, el final de la mentira. Aquí está la lección profunda: no existe una forma canónica de hacer optimismo, existen dos posturas sobre cuánto debe durar una verdad provisional y quién es responsable de retirarla. Si la predicción es una edición a un estado compartido que persiste, tú eres el responsable y la memoria del pasado —el snapshot— es tu deber. Si la predicción es una superposición temporal sobre una verdad que se reafirmará sola, el framework es el responsable y tu deber es solo describir cómo se ve mientras dura. Elegir entre onMutate y useOptimistic no es, entonces, una cuestión de gusto ni de qué librería usas, sino de responder a una pregunta anterior: la predicción que vas a mostrar, cuando termine su momento, ¿tiene que ser recordada y deshecha, o basta con que sea olvidada?
- Implementa una lista de tareas con TanStack Query usando el trío
onMutate,onErroryonSettled, y verifica que el contexto transporta el snapshot hasta el rollback. - Fuerza un fallo del servidor y comprueba que
onErrorrestaura yonSettledsigue invalidando para reconciliar. - Reimplementa la misma lista con
useOptimisticdentro de una acción, y observa que no escribes ningún rollback porque el estado optimista se descarta solo al terminar la transición. - Provoca un fallo en la versión con
useOptimisticy razona por qué la predicción desaparece sin que hayas restaurado nada a mano. - Argumenta, para un caso real de tu aplicación, cuál de las dos filosofías encaja: ¿la predicción debe sobrevivir a la acción y compartirse entre vistas, o basta con que viva mientras dura una transición?
- Intenta usar
useOptimisticfuera de una acción ostartTransitiony observa que la capa se ignora; extrae de ahí la regla de cuándo esta API no es la herramienta adecuada.