Formulários Otimistas
Use useOptimistic com formulários para mostrar feedback instantâneo enquanto as ações do servidor processam - com UI pendente e rollback automático em caso de falha.
Busque em todas as páginas da documentação
Use useOptimistic com formulários para mostrar feedback instantâneo enquanto as ações do servidor processam - com UI pendente e rollback automático em caso de falha.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
"use client";
import { useOptimistic, useActionState } from "react";
type Todo = { id: string; text: string; completed: boolean };
function TodoList({
todos,
toggleAction,
}: {
todos: Todo[];
toggleAction: (formData: FormData) => Promise<void>;
}) {
const [optimisticTodos, setOptimistic] = useOptimistic(
todos,
(state, toggledId: string) =>
state.map((t) => (t.id === toggledId ? { ...t, completed: !t.completed } : t))
);
return (
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>
<form
action={async (formData) => {
setOptimistic(todo.id);
await toggleAction(formData);
}}
>
<input type="hidden" name="id" value={todo.id} />
<button type="submit" className={todo.completed ? "line-through opacity-50" : ""}>
{todo.text}
</button>
</form>
</li>
))}
</ul>
);
}Quando usar isso: Quando a ação do servidor provavelmente terá sucesso e você quer que a UI pareça instantânea - alternar, curtir, excluir ou reordenar itens.
// app/actions/messages.ts
"use server";
import { revalidatePath } from "next/cache";
export type Message = {
id: string;
text: string;
author: string;
createdAt: string;
pending?: boolean;
};
export async function addMessage(prevState: any, formData: FormData) {
const text = formData.get("text") as string;
if (!text?.trim()) return { error: "A mensagem não pode estar vazia" };
// Simula atraso de rede
await new Promise((r) => setTimeout(r, 1500));
// Simula falha ocasional
if (Math.random() < 0.2) {
return { error: "Falha ao enviar. Tente novamente." };
}
await db.message.create({
data: { text, author: "You", createdAt: new Date().toISOString() },
});
revalidatePath("/chat");
return { success: true };
}
export async function deleteMessage(formData: FormData) {
const id = formData.get("id") as string;
await db.message.delete({ where: { id } });
revalidatePath("/chat");
}// app/chat/page.tsx
"use client";
import { useOptimistic, useActionState, useRef } from "react";
import { addMessage, deleteMessage, type Message } from "@/app/actions/messages";
export function ChatRoom({ messages }: { messages: Message[] }) {
const formRef = useRef<HTMLFormElement>(null);
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, action: { type: "add"; message: Message } | { type: "delete"; id: string }) => {
if (action.type === "add") return [...state, action.message];
if (action.type === "delete") return state.filter((m) => m.id !== action.id);
return state;
}
);
const [sendState, sendAction, isSending] = useActionState(
async (prev: any, formData: FormData) => {
const text = formData.get("text") as string;
addOptimistic({
type: "add",
message: {
id: `temp-${Date.now()}`,
text,
author: "You",
createdAt: new Date().toISOString(),
pending: true,
},
});
formRef.current?.reset();
return addMessage(prev, formData);
},
null
);
return (
<div className="mx-auto max-w-lg">
<div className="space-y-3 rounded border p-4" style={{ minHeight: 300 }}>
{optimisticMessages.map((msg) => (
<div
key={msg.id}
className={`flex items-start justify-between rounded p-2 ${
msg.pending ? "bg-blue-50 opacity-60" : "bg-gray-50"
}`}
>
<div>
<span className="text-xs font-medium text-gray-500">{msg.author}</span>
<p className="text-sm">{msg.text}</p>
{msg.pending && <span className="text-xs text-blue-500">Enviando...</span>}
</div>
{!msg.pending && (
<form
action={async (formData) => {
addOptimistic({ type: "delete", id: msg.id });
await deleteMessage(formData);
}}
>
<input type="hidden" name="id" value={msg.id} />
<button type="submit" className="text-xs text-red-400 hover:text-red-600">
Excluir
</button>
</form>
)}
</div>
))}
</div>
{sendState?.error && (
<p className="mt-2 text-sm text-red-600">{sendState.error}</p>
)}
<form ref={formRef} action={sendAction} className="mt-3 flex gap-2">
<input
name="text"
placeholder="Digite uma mensagem..."
className="flex-1 rounded border p-2"
required
/>
<button
type="submit"
disabled={isSending}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
Enviar
</button>
</form>
</div>
);
}O que isso demonstra:
useOptimistic com um atualizador estilo reducer para ações de adicionar e excluirmessages realuseOptimistic(serverState, updaterFn) retorna [optimisticState, setOptimistic]setOptimistic(value) é chamado, React aplica a função atualizadora para produzir um estado temporárioserverState da prop/paiserverState originaluseOptimistic só funciona dentro de uma transição (ação de formulário ou startTransition)Botão de curtir com contagem otimista:
function LikeButton({ postId, likes, isLiked }: { postId: string; likes: number; isLiked: boolean }) {
const [optimistic, setOptimistic] = useOptimistic(
{ likes, isLiked },
(state, _: void) => ({
likes: state.isLiked ? state.likes - 1 : state.likes + 1,
isLiked: !state.isLiked,
})
);
return (
<form action={async () => {
setOptimistic(undefined);
await toggleLike(postId);
}}>
<button type="submit">
{optimistic.isLiked ? "heart-filled" : "heart"} {optimistic.likes}
</button>
</form>
);
}Reordenação otimista:
const [optimisticItems, reorder] = useOptimistic(
items,
(state, { from, to }: { from: number; to: number }) => {
const next = [...state];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
return next;
}
);// useOptimistic é genérico
const [state, setState] = useOptimistic<Message[], { type: "add"; message: Message }>(
messages,
(state, action) => {
// action é tipado como { type: "add"; message: Message }
return [...state, action.message];
}
);
// A função atualizadora deve retornar o mesmo tipo do primeiro argumento
// (state: Message[], action: Action) => Message[]Só funciona em transições - Chamar setOptimistic fora de uma ação de formulário ou startTransition não tem efeito. Correção: Certifique-se de chamá-lo dentro de uma ação de formulário assíncrona ou envolva com startTransition.
Rollback substitui todo o estado - Quando a ação falha, todo o estado otimista reverte, não apenas o item falho. Este é o comportamento correto, mas pode surpreendê-lo se várias ações estiverem em andamento.
Nenhuma API de rollback manual - Você não pode reverter manualmente o estado otimista. Correção: O rollback automático lida com falhas. Para controle manual, use useState regular com try/catch.
Closure obsoleto no atualizador - O atualizador recebe o estado otimista atual (incluindo atualizações otimistas anteriores), então chamadas sequenciais se compõem corretamente. Mas evite fechar sobre estado externo.
Re-renderização do componente servidor necessária - Após a conclusão da ação, o componente servidor pai deve re-renderizar com novos dados (via revalidatePath ou revalidateTag) para que o estado otimista se resolva corretamente.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
useState + try/catch | Você precisa de controle de rollback manual | Você quer rollback automático |
SWR optimisticData | Você usa SWR para buscar dados | Você usa Componentes de Servidor e ações |
TanStack Query onMutate | Você usa TanStack Query com atualizações otimistas | Você usa o padrão App Router |
| Sem UI otimista | A ação é rápida (menos de 200ms) ou a falha é provável | Usuários percebem lentidão e a ação geralmente tem sucesso |
[optimisticState, setOptimistic]optimisticState reflete o estado temporário da UI enquanto uma ação está pendentesetOptimistic(value) dentro de uma ação de formulário ou startTransition para aplicar a atualização otimistaserverState realuseOptimistic só produz estado temporário durante uma transição pendentesetOptimistic fora de uma ação de formulário ou startTransition não tem efeitostartTransition só é necessário fora dos formuláriosconst message = { ...data, id: `temp-${Date.now()}`, pending: true };
addOptimistic({ type: "add", message });
// Na UI:
{msg.pending && <span className="text-blue-500">Enviando...</span>}pending a itens otimistas e estilize-os de forma diferente (opacidade, rótulo)(currentState, actionPayload) e retorna o novo estado otimista{ type: "add"; message: Message } | { type: "delete"; id: string }revalidatePath ou revalidateTag na ação do servidor para acionar uma re-renderizaçãoconst [state, setState] = useOptimistic<
Message[],
{ type: "add"; message: Message } | { type: "delete"; id: string }
>(messages, (state, action) => {
// action é totalmente tipado
if (action.type === "add") return [...state, action.message];
if (action.type === "delete") return state.filter(m => m.id !== action.id);
return state;
});(state: T, action: A) => T - o tipo de retorno corresponde ao tipo de estadouseState com try/catch em vez dissoreset() imediatamente após setOptimistic limpa o campo de entradaRevisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥