Busque em todas as páginas da documentação
🤖 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>
);
}Quando usar isso: Use useOptimistic sempre que quiser que a UI seja atualizada instantaneamente enquanto uma operação assíncrona (ação de servidor, chamada de API) está em andamento - curtidas, mensagens, alternâncias, atualizações de carrinho, qualquer mutação onde o usuário não deva esperar.
// Uma lista de tarefas com adição, alternância e exclusão otimistas
"use client";
import { useOptimistic, useActionState, useRef } from "react";
type Todo = {
id: string;
text: string;
completed: boolean;
pending?: boolean;
deleting?: boolean;
};
// Simula ações 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">Todos</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">
Excluir
</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);
// Em um aplicativo real, a revalidação atualizaria os todos
}}>
<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">Adicionar</button>
</div>
</form>
</div>
);
}O que isso demonstra:
useOptimistic lidando com três tipos de ação diferentes (adicionar, alternar, excluir)deletingtodos quando a ação é concluídauseOptimistic(passthrough, updateFn) retorna [optimisticState, addOptimistic].
passthrough é a fonte de dados real (por exemplo, de props ou useActionState). Quando nenhuma ação está em andamento, optimisticState === passthrough.updateFn(currentState, optimisticValue) é uma função pura que produz a versão otimista do estado.addOptimistic(value) aciona updateFn imediatamente, fazendo com que a UI seja atualizada antes que o trabalho assíncrono termine.passthrough atualizado. Não há etapa manual de "commit" ou "rollback".passthrough original. O usuário vê a alteração "desfazer-se".useOptimistic é projetado para funcionar com o sistema de transição e ação do React. Chamar addOptimistic fora de uma ação ou transição não tem efeito.addOptimistic durante a mesma ação são agrupadas. O updateFn recebe o estado otimista acumulado.Alternância booleana simples:
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 ? "Descurtir" : "Curtir"}</button>
</form>
);
}Com useActionState para estado de formulário combinado e UI otimista:
"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">Adicionar ao Carrinho ({optimisticCount})</button>
</form>
);
}useOptimistic<State, Action>(passthrough: State, updateFn: (state: State, action: Action) => State) retorna [State, (action: Action) => void].updateFn é fornecido, o segundo argumento para addOptimistic substitui o estado diretamente: useOptimistic<State>(passthrough: State) retorna [State, (newState: State) => void].Action controla o que você passa para addOptimistic. Use uma união discriminada para múltiplos tipos de ação.addOptimistic dentro de uma ação de formulário, ação de servidor ou callback startTransition.updateFn deve ser puro. Mutar o array/objeto de estado atual causa bugs. Correção: Sempre retorne um novo array/objeto: [...state, newItem].useActionState para mostrar uma mensagem de erro.updateFn para lidar corretamente com o estado acumulado.| Abordagem | Quando escolher |
|---|---|
useOptimistic | Integrado ao React 19, funciona com ações de formulário e transições |
TanStack Query useMutation com onMutate | Necessidade de cache, retentativa e rollback sofisticado |
SWR mutate com optimisticData | Já usando SWR para busca de dados |
useState manual para alternância | Casos simples onde você gerencia o estado pendente sozinho |
| Redux Toolkit atualizações otimistas | Grande aplicativo Redux com middleware existente |
[optimisticState, addOptimistic]optimisticState é igual ao valor passthrough quando nenhuma ação está em andamentoaddOptimistic(value) aciona updateFn imediatamente para produzir uma versão otimista do estadopassthrough atualizadopassthrough originaluseActionState para mostrar uma mensagem de erro, pois o rollback é silenciosoaddOptimistic deve ser chamado dentro de uma ação de formulário, ação de servidor ou callback startTransitionstartTransition se não estiver usando um formulárioUse uma união discriminada para o tipo de ação:
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 ? "Descurtir" : "Curtir"}</button>
</form>
);
}Quando nenhum updateFn é fornecido, addOptimistic substitui o estado diretamente.
pending: true ou sending: true no valor de retorno do updateFnopacity-50, texto em itálico, rótulo "(enviando...)")updateFn recebe o estado otimista acumulado de chamadas anterioresupdateFn para lidar corretamente com o estado acumulado para evitar conflitospassthrough substitui o estado otimista quando a ação terminaupdateFn deve ser puro -- mutar o estado atual causa bugs[...state, newItem] em vez de state.push(newItem)useOptimistic<State, Action>(
passthrough: State,
updateFn: (state: State, action: Action) => State
): [State, (action: Action) => void]Use uma união discriminada para o tipo Action para suportar múltiplos tipos de ação.
useOptimistic<State>(passthrough: State) retorna [State, (newState: State) => void]addOptimistic substitui o estado diretamenteRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥