Hook useOptimistic
Muestra un estado optimista (predicho) mientras una acción asincrónica está en progreso, luego se reconcilia cuando se completa.
Receta
Tarjeta de referencia rápida — lista para copiar-pegar.
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(currentState, newMessage: string) => [
...currentState,
{ text: newMessage, sending: true },
]
);
// Llama dentro de una acción o transición
startTransition(() => {
addOptimistic("Hello!");
await sendMessage("Hello!");
});Cuándo usarlo: Quieres que la interfaz se actualice instantáneamente cuando un usuario realiza una acción (como enviar un mensaje, dar me gusta a una publicación o agregar un elemento) mientras el servidor procesa la solicitud en el fondo.
Ejemplo trabajado
"use client";
import { useOptimistic, useActionState, useRef } from "react";
interface Message {
id: number;
text: string;
sending?: boolean;
}
async function sendMessageAction(
prevState: Message[],
formData: FormData
): Promise<Message[]> {
const text = formData.get("message") as string;
// Simula retraso del servidor
await new Promise((resolve) => setTimeout(resolve, 1500));
return [
...prevState,
{ id: Date.now(), text, sending: false },
];
}
export function Chat() {
const [messages, formAction, isPending] = useActionState(sendMessageAction, [
{ id: 1, text: "¡Bienvenido al chat!", sending: false },
]);
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: Date.now(), text: newMessage, sending: true },
]
);
const formRef = useRef<HTMLFormElement>(null);
async function handleSubmit(formData: FormData) {
const text = formData.get("message") as string;
if (!text.trim()) return;
formRef.current?.reset();
addOptimistic(text);
await formAction(formData);
}
return (
<div className="space-y-3 max-w-sm">
<ul className="space-y-2">
{optimisticMessages.map((msg) => (
<li
key={msg.id}
className={`text-sm px-3 py-2 rounded ${
msg.sending
? "bg-gray-100 text-gray-400 italic"
: "bg-blue-50 text-gray-900"
}`}
>
{msg.text}
{msg.sending && <span className="ml-2 text-xs">(enviando...)</span>}
</li>
))}
</ul>
<form ref={formRef} action={handleSubmit} className="flex gap-2">
<input
name="message"
className="flex-1 border rounded px-3 py-2"
placeholder="Escribe un mensaje..."
required
/>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
Enviar
</button>
</form>
</div>
);
}Lo que esto demuestra:
- Los mensajes aparecen instantáneamente con un estado visual de "enviando"
- Una vez que la acción del servidor se completa, el estado optimista se reemplaza por el estado real
- Si la acción falla, el estado optimista se revierte automáticamente
- El formulario se reinicia inmediatamente para una sensación fluida, no después de que el servidor responde
Profundización
Cómo funciona
useOptimistictoma el estado actual y una función de actualización que describe cómo aplicar un cambio optimista- Cuando llamas a
addOptimistic(value), React inmediatamente muestra el estado optimista fusionado - El estado optimista es temporal — superpone el estado actual solo mientras una acción asincrónica (transición) está pendiente
- Cuando la acción se completa y el estado actual se actualiza, la superposición optimista se elimina y el estado real toma el control
- Si la acción falla, el estado optimista se descarta automáticamente, revirtiendo al estado actual
Parámetros y valores de retorno
| Parámetro | Tipo | Descripción |
|---|---|---|
state | T | El valor del estado actual (fuente de verdad) |
updateFn | (currentState: T, optimisticValue: V) => T | Función pura que fusiona el valor optimista en el estado actual |
| Retorno | Tipo | Descripción |
|---|---|---|
optimisticState | T | Estado actual con actualizaciones optimistas aplicadas (igual a state cuando ninguna acción está pendiente) |
addOptimistic | (value: V) => void | Función para desencadenar una actualización optimista |
Variaciones
Botón de me gusta optimista:
const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: null) => current + 1
);
async function handleLike() {
startTransition(async () => {
addLike(null);
await likePost(postId);
});
}
return (
<button onClick={handleLike}>
{optimisticLikes} Gustos
</button>
);Alternar todo optimista:
const [optimisticTodos, toggleOptimistic] = useOptimistic(
todos,
(state, toggledId: number) =>
state.map((todo) =>
todo.id === toggledId ? { ...todo, done: !todo.done } : todo
)
);Eliminar optimista:
const [optimisticItems, removeOptimistic] = useOptimistic(
items,
(state, removedId: string) => state.filter((item) => item.id !== removedId)
);Notas de TypeScript
// Los tipos genéricos se infieren de los parámetros
const [optimistic, add] = useOptimistic(
messages, // T = Message[]
(state, text: string) => ... // V = string
);
// add: (value: string) => void
// optimistic: Message[]
// Genéricos explícitos cuando sea necesario
const [optimistic, add] = useOptimistic<Todo[], number>(
todos,
(state, toggledId) => state.map(t =>
t.id === toggledId ? { ...t, done: !t.done } : t
)
);Errores comunes
-
Llamar a addOptimistic fuera de una transición — El estado optimista solo funciona correctamente dentro de una acción asincrónica o
startTransition. Fuera, la superposición se elimina inmediatamente. Solución: Siempre llama aaddOptimisticdentro destartTransitiono una acción de formulario. -
Mutar state en updateFn — Mutar el array de estado actual (p. ej.,
state.push(item)) causa errores. Solución: Retorna un nuevo array u objeto desdeupdateFn. -
Sin callback de error — No hay una forma integrada de mostrar un toast de error cuando el estado optimista se revierte. Solución: Maneja errores en tu función de acción y actualiza el estado con un mensaje de error.
-
Múltiples actualizaciones optimistas rápidas — Cada llamada a
addOptimisticse aplica sobre el estado optimista anterior, lo que puede dar resultados inesperados si la función de actualización no es componible. Solución: Diseña tuupdateFnpara ser idempotente o aditiva. -
Estado actual obsoleto — Si el prop de estado actual cambia de una fuente diferente mientras una acción está pendiente, la superposición optimista se recalcula sobre el nuevo estado actual. Solución: Este es generalmente el comportamiento correcto, pero ten cuidado al depurar.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
useState con reversión manual | Necesitas lógica de reversión personalizada o manejo de errores | Patrones optimistas simples donde la reversión automática es suficiente |
useTransition con isPending | Solo necesitas un indicador de carga, no un valor optimista | Quieres que la interfaz refleje el resultado esperado inmediatamente |
TanStack Query onMutate | Usas TanStack Query y necesitas actualizaciones optimistas con invalidación de caché | Estás usando acciones de servidor y quieres una solución ligera |
| Deshabilitar e indicador | La simplicidad se prefiere y la latencia es baja | Los usuarios esperan retroalimentación instantánea |
¿Por qué useOptimistic sobre estado manual? useOptimistic se revierte automáticamente en caso de fallo y se fusiona correctamente con el estado actual cuando la acción se completa. Las implementaciones manuales son propensas a errores y verbosas.
Preguntas Frecuentes
¿Qué sucede con el estado optimista si la acción asincrónica falla?
- El estado optimista se descarta automáticamente cuando la acción se completa (éxito o fallo).
- La interfaz revierte al estado actual, efectivamente "revirtiendo" la actualización optimista.
- No hay un callback de error integrado -- maneja errores en tu función de acción.
¿Por qué debo llamar a addOptimistic dentro de una transición o acción de formulario?
- La superposición optimista solo persiste mientras una acción asincrónica (transición) está pendiente.
- Si se llama fuera de una transición, la superposición se elimina inmediatamente en el siguiente renderizado.
- Siempre usa
startTransitiono una acción de formulario para mantener el estado optimista visible.
¿En qué se diferencia useOptimistic de manejar estado optimista manualmente con useState?
useOptimisticse revierte automáticamente en caso de fallo y se fusiona correctamente cuando el estado actual se actualiza.- Las implementaciones manuales requieren que rastrees el estado original, manejes la reversión y fusiones los resultados tú mismo.
useOptimistices menos propensa a errores para patrones comunes como agregar, alternar o eliminar elementos.
Trampa: ¿Qué sucede si muto el array de estado dentro de updateFn?
- Mutar el array (p. ej.,
state.push(item)) causa errores porque React espera actualizaciones inmutables. - Siempre retorna un nuevo array u objeto desde
updateFn.
// Incorrecto
(state, newItem) => { state.push(newItem); return state; }
// Correcto
(state, newItem) => [...state, newItem]¿Puedo llamar a addOptimistic varias veces antes de que se complete la acción?
- Sí. Cada llamada se aplica sobre el estado optimista anterior.
- Asegúrate de que tu
updateFnsea componible -- cada llamada debe producir un resultado válido cuando se estratifica. - Todas las superposiciones optimistas se descartan cuando la acción se completa.
¿Cómo muestro un toast de error cuando una actualización optimista se revierte?
- No hay un callback integrado para la detección de reversión.
- Maneja errores en tu función de acción y actualiza el estado actual con un mensaje de error.
- Compara el estado actual antes y después de la acción para detectar una reversión en la interfaz.
¿Cómo le doy tipo a useOptimistic con TypeScript?
const [optimistic, add] = useOptimistic(
messages, // T = Message[]
(state, text: string) => [ // V = string
...state,
{ id: Date.now(), text, sending: true },
]
);
// add: (value: string) => void
// optimistic: Message[]Trampa: ¿Qué sucede si el estado actual cambia de una fuente diferente mientras una acción está pendiente?
- La superposición optimista se recalcula sobre el nuevo estado actual.
- Este es generalmente el comportamiento correcto -- la superposición se adapta a la realidad más reciente.
- Ten cuidado al depurar valores optimistas inesperados.
¿Puedo usar useOptimistic para un toggle de me gusta/no me gusta?
const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: null) => current + 1
);
async function handleLike() {
startTransition(async () => {
addLike(null);
await likePost(postId);
});
}- Sí. El valor optimista se muestra inmediatamente; el recuento real se actualiza cuando el servidor responde.
¿Cuándo debo usar useOptimistic en lugar de solo mostrar un spinner con isPending?
- Usa
useOptimisticcuando los usuarios esperan retroalimentación instantánea (mensajes, me gusta, toggles). - Usa un spinner cuando el resultado es incierto y mostrar un estado predicho podría ser engañoso.
- Para acciones de baja latencia, un spinner es suficiente; para acciones de alta latencia, la interfaz optimista se siente mucho más rápida.
Relacionado
- useActionState — maneja el estado del formulario que impulsa actualizaciones optimistas
- useTransition — las transiciones alimentan el estado pendiente en el que se basan las actualizaciones optimistas
- use — primitiva de React 19 para consumir valores asincronos
- useState — el estado subyacente que
useOptimisticsuperpone