Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
import { useOptimistic } from "react";
type Message = { id: string; text: string; sending?: boolean };
function Chat({
messages,
sendMessage,
}: {
messages: Message[];
sendMessage: (text: string) => Promise<void>;
}) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newText: string) => [
...state,
{ id: "temp-" + Date.now(), text: newText, sending: true },
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get("text") as string;
addOptimistic(text);
await sendMessage(text);
}
return (
<div>
<ul>
{optimisticMessages.map((msg) => (
<li key={msg.id} className={msg.sending ? "opacity-50" : ""}>
{msg.text}
{msg.sending && " (enviando...)"}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" required />
<button type="submit">Enviar</button>
</form>
</div>
);
}Cuándo usarlo: Usa useOptimistic siempre que quieras que la interfaz de usuario se actualice instantáneamente mientras una operación asincrónica (acción de servidor, llamada a API) está en tránsito -me gusta, mensajes, alternancias, actualizaciones de carrito, cualquier mutación donde el usuario no debería esperar.
// Una lista de tareas con adición, alternancia y eliminación optimista
"use client";
import { useOptimistic, useActionState, useRef } from "react";
type Todo = {
id: string;
text: string;
completed: boolean;
pending?: boolean;
deleting?: boolean;
};
// Simular acciones de servidor
async function serverAddTodo(text: string): Promise<Todo> {
await new Promise((r) => setTimeout(r, 1000));
return { id: crypto.randomUUID(), text, completed: false };
}
async function serverToggleTodo(id: string): Promise<void> {
await new Promise((r) => setTimeout(r, 500));
}
async function serverDeleteTodo(id: string): Promise<void> {
await new Promise((r) => setTimeout(r, 500));
}
type OptimisticAction =
| { type: "add"; text: string }
| { type: "toggle"; id: string }
| { type: "delete"; id: string };
export default function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [todos, setTodos] = useActionState(
async (_prev: Todo[], formData: FormData) => {
const text = formData.get("text") as string;
addOptimistic({ type: "add", text });
const newTodo = await serverAddTodo(text);
return [..._prev, newTodo];
},
initialTodos
);
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state: Todo[], action: OptimisticAction) => {
switch (action.type) {
case "add":
return [...state, { id: "temp", text: action.text, completed: false, pending: true }];
case "toggle":
return state.map((t) =>
t.id === action.id ? { ...t, completed: !t.completed, pending: true } : t
);
case "delete":
return state.map((t) =>
t.id === action.id ? { ...t, deleting: true } : t
);
}
}
);
const formRef = useRef<HTMLFormElement>(null);
async function handleToggle(id: string) {
addOptimistic({ type: "toggle", id });
await serverToggleTodo(id);
}
async function handleDelete(id: string) {
addOptimistic({ type: "delete", id });
await serverDeleteTodo(id);
}
return (
<div className="max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-4">Tareas</h1>
<ul className="space-y-2">
{optimisticTodos
.filter((t) => !t.deleting)
.map((todo) => (
<li
key={todo.id}
className={`flex items-center gap-2 ${todo.pending ? "opacity-50" : ""}`}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span className={todo.completed ? "line-through" : ""}>{todo.text}</span>
<button onClick={() => handleDelete(todo.id)} className="ml-auto text-red-500">
Eliminar
</button>
</li>
))}
</ul>
<form ref={formRef} action={async (formData) => {
const text = formData.get("text") as string;
addOptimistic({ type: "add", text });
formRef.current?.reset();
const newTodo = await serverAddTodo(text);
// En una aplicación real, la revalidación actualizaría las tareas
}}>
<div className="flex gap-2 mt-4">
<input name="text" required className="border p-2 rounded flex-1" />
<button type="submit" className="bg-blue-500 text-white px-4 rounded">Agregar</button>
</div>
</form>
</div>
);
}Lo que demuestra esto:
useOptimistic que maneja tres tipos de acciones diferentes (agregar, alternar, eliminar)deletingtodos cuando la acción se completauseOptimistic(passthrough, updateFn) devuelve [optimisticState, addOptimistic].
passthrough es la fuente de datos real (por ejemplo, de props o useActionState). Cuando no hay acción en tránsito, optimisticState === passthrough.updateFn(currentState, optimisticValue) es una función pura que produce la versión optimista del estado.addOptimistic(value) activa updateFn inmediatamente, haciendo que la interfaz de usuario se actualice antes de que el trabajo asincrónico se complete.passthrough actualizado. No hay ningún paso manual de "confirmar" o "retroceder".passthrough. El usuario ve que el cambio se "deshace" a sí mismo.useOptimistic está diseñado para funcionar con el sistema de transición y acción de React. Llamar a addOptimistic fuera de una acción o transición no tiene efecto.addOptimistic durante la misma acción se agrupan por lotes. updateFn recibe el estado optimista acumulado.Simple alternancia de booleano:
function LikeButton({ isLiked, onToggle }: { isLiked: boolean; onToggle: () => Promise<void> }) {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
return (
<form action={async () => {
setOptimisticLiked(!optimisticLiked);
await onToggle();
}}>
<button type="submit">{optimisticLiked ? "No me gusta" : "Me gusta"}</button>
</form>
);
}Con useActionState para estado de formulario combinado e interfaz de usuario optimista:
"use client";
import { useActionState, useOptimistic } from "react";
import { addToCart } from "./actions";
function CartButton({ count }: { count: number }) {
const [serverCount, action, isPending] = useActionState(addToCart, count);
const [optimisticCount, setOptimisticCount] = useOptimistic(serverCount);
return (
<form action={async (formData) => {
setOptimisticCount((c) => c + 1);
await action(formData);
}}>
<button type="submit">Agregar al carrito ({optimisticCount})</button>
</form>
);
}useOptimistic<State, Action>(passthrough: State, updateFn: (state: State, action: Action) => State) devuelve [State, (action: Action) => void].updateFn, el segundo argumento a addOptimistic reemplaza el estado directamente: useOptimistic<State>(passthrough: State) devuelve [State, (newState: State) => void].Action controla qué pasas a addOptimistic. Usa una unión discriminada para múltiples tipos de acciones.addOptimistic dentro de una acción de formulario, acción de servidor o devolución de llamada startTransition.updateFn debe ser puro. Mutar la matriz/objeto de estado actual causa errores. Solución: Siempre devuelve una nueva matriz/objeto: [...state, newItem].useActionState para mostrar un mensaje de error.updateFn para manejar correctamente el estado acumulado.| Enfoque | Cuándo elegir |
|---|---|
useOptimistic | React 19 integrado, funciona con acciones de formulario y transiciones |
TanStack Query useMutation con onMutate | Necesitas almacenamiento en caché, reintentos y retroceso sofisticado |
SWR mutate con optimisticData | Ya estás usando SWR para obtención de datos |
useState manual | Casos simples donde tú mismo administras el estado pendiente |
| Redux Toolkit actualizaciones optimistas | Aplicación Redux grande con middleware existente |
[optimisticState, addOptimistic]optimisticState es igual al valor passthrough cuando no hay acción en tránsitoaddOptimistic(value) activa updateFn inmediatamente para producir una versión optimista del estadopassthrough actualizadopassthroughuseActionState para mostrar un mensaje de error, ya que el retroceso es silenciosoaddOptimistic debe ser llamado dentro de una acción de formulario, acción de servidor o devolución de llamada startTransitionstartTransition si no estás usando un formularioUsa una unión discriminada para el tipo de acción:
type Action =
| { type: "add"; text: string }
| { type: "toggle"; id: string }
| { type: "delete"; id: string };
const [optimistic, dispatch] = useOptimistic(
todos,
(state, action: Action) => {
switch (action.type) {
case "add": return [...state, { id: "temp", text: action.text }];
case "toggle": return state.map(t => t.id === action.id ? { ...t, completed: !t.completed } : t);
case "delete": return state.filter(t => t.id !== action.id);
}
}
);function LikeButton({ isLiked, onToggle }) {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
return (
<form action={async () => {
setOptimisticLiked(!optimisticLiked);
await onToggle();
}}>
<button type="submit">{optimisticLiked ? "No me gusta" : "Me gusta"}</button>
</form>
);
}Cuando no se proporciona updateFn, addOptimistic reemplaza el estado directamente.
pending: true o sending: true en el valor de retorno de updateFnopacity-50, texto en cursiva, etiqueta "(enviando...)")updateFn recibe el estado optimista acumulado de llamadas anterioresupdateFn para manejar correctamente el estado acumulado y evitar conflictospassthrough reemplaza el estado optimista cuando la acción se completaupdateFn debe ser puro -- mutar el estado actual causa errores[...state, newItem] en lugar de state.push(newItem)useOptimistic<State, Action>(
passthrough: State,
updateFn: (state: State, action: Action) => State
): [State, (action: Action) => void]Usa una unión discriminada para el tipo Action para admitir múltiples tipos de acciones.
useOptimistic<State>(passthrough: State) devuelve [State, (newState: State) => void]addOptimistic reemplaza el estado directamenteRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥