useTransition vs useOptimistic en React: no son lo mismo
Los dos hacen que tu UI se sienta más rápida durante una mutación, pero resuelven problemas distintos. Confundirlos produce UIs que parecen lentas sin serlo.
Ambos hooks existen para lo mismo, en teoría: hacer que tu interfaz se sienta rápida mientras una Server Action está en curso. Y sin embargo, usarlos indistintamente produce experiencias notablemente distintas — una se siente “con un pequeño delay pero clara”, la otra se siente “instantánea, sin fricción”. Aquí la diferencia exacta, con dos casos reales de un mismo carrito de compras.
useTransition: te dice que algo está en curso, no cambia lo que se muestra
'use client'
import { useTransition } from 'react'
import { addToCart } from '@/actions/cart.actions'
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
function handleClick() {
startTransition(async () => {
await addToCart(productId)
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Agregando...' : 'Agregar al carrito'}
</button>
)
}
useTransition te da un booleano (isPending) que puedes usar para mostrar un estado de carga — deshabilitar el botón, cambiar el texto, mostrar un spinner. Pero no cambia el dato que se muestra en pantalla hasta que la Server Action termina de verdad y el servidor confirma el nuevo estado. Es honesto: “esto está en curso”, sin fingir un resultado que todavía no existe.
Para un botón donde el usuario no está viendo el resultado de su acción en el mismo lugar (agregar algo a un carrito que vive en otra página), esto es suficiente y honesto. No necesitas simular nada — el usuario solo quiere confirmación de que su click se registró.
useOptimistic: muestra el resultado antes de que se confirme
'use client'
import { useOptimistic, useTransition } from 'react'
import { updateCartItemQuantity } from '@/actions/cart.actions'
export function CartItemRow({ item }: { item: { id: string; quantity: number } }) {
const [isPending, startTransition] = useTransition()
const [optimisticQuantity, setOptimisticQuantity] = useOptimistic(
item.quantity,
(_current, newQuantity: number) => newQuantity
)
function handleChange(newQuantity: number) {
startTransition(async () => {
setOptimisticQuantity(newQuantity) // se muestra YA, antes de confirmar
await updateCartItemQuantity(item.id, newQuantity)
})
}
return (
<div>
<button onClick={() => handleChange(optimisticQuantity - 1)}>-</button>
<span>{optimisticQuantity}</span>
<button onClick={() => handleChange(optimisticQuantity + 1)}>+</button>
</div>
)
}
Acá el usuario sí está mirando directamente el resultado de su acción (la cantidad, en la misma fila). useOptimistic toma un valor base (item.quantity, el real, del servidor) y una función reductora — parecido a useReducer — que calcula el valor “optimista” a mostrar de inmediato, sin esperar la confirmación del servidor. Cuando la Server Action termina y Next.js revalida la ruta, React reconcilia: si el valor optimista coincidía con lo que realmente pasó, no hay ningún parpadeo. Si la mutación falla, React revierte automáticamente al valor real.
La regla para elegir entre uno y otro
| Pregunta | Si la respuesta es sí |
|---|---|
| ¿El usuario ve el resultado de su acción en el mismo lugar donde actuó? | useOptimistic |
| ¿Solo necesita saber que “algo está pasando”, sin ver el resultado ahí mismo? | useTransition solo |
| ¿La UI ya muestra un valor que va a cambiar (cantidad, texto, estado)? | useOptimistic |
| ¿Es una acción de “dispara y olvida” (enviar, agregar, eliminar sin ver el efecto inmediato)? | useTransition solo |
Un matiz importante: useOptimistic casi siempre se usa junto con useTransition, no en lugar de. startTransition es lo que envuelve la llamada async a la Server Action; useOptimistic es lo que decide qué mostrar mientras tanto.
El error común: usar useOptimistic en todos lados “porque se ve mejor”
No siempre es gratis. useOptimistic asume que puedes predecir el resultado antes de que el servidor lo confirme — funciona perfecto para “incrementar una cantidad” (predices bien: será justo ese número). Pero si la mutación depende de una validación de servidor no trivial (por ejemplo, “actualiza el stock, pero solo si todavía hay suficiente”), mostrar un valor optimista que luego se revierte puede confundir más de lo que ayuda. En esos casos, un isPending honesto con useTransition comunica mejor lo que realmente está pasando.
Cómo se ve el “flash” cuando algo sale mal
Un detalle práctico que vale la pena mencionar: si combinas useOptimistic con un cambio de opacidad u otro indicador visual de isPending sin una transición CSS suave, el cambio se siente como un parpadeo brusco en vez de una animación natural.
// Con transición suave, el cambio de estado no se siente como un salto
<div style={{ opacity: isPending ? 0.6 : 1, transition: 'opacity 0.15s ease' }}>
{/* contenido de la fila */}
</div>
Un detalle pequeño, pero es la diferencia entre que la UI se sienta pulida o se sienta “con bugs” aunque funcionalmente esté correcta.
Conclusión
useTransition comunica que algo está en curso. useOptimistic predice y muestra el resultado antes de que se confirme. No compiten — normalmente trabajan juntos, pero elegir cuál protagoniza tu interacción depende de si el usuario está mirando directamente el resultado de lo que acaba de hacer. Usar el equivocado no rompe nada funcionalmente, pero sí hace que tu UI se sienta más lenta o más confusa de lo necesario.
¿Quieres mejorar la percepción de velocidad de tu aplicación React/Next.js sin reescribirla? Agenda una asesoría técnica gratuita de 15 minutos.
¿Tienes un proyecto en mente?
Convierte tu idea en un producto real
Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.