Atualizações de UI otimistas no React deixam apps instantâneos. Aprenda padrões seguros para reconciliar com o servidor, lidar com falhas e prevenir deriva de dados.

UI otimista no React significa que você atualiza a tela como se uma mudança já tivesse sido aceita, antes do servidor confirmar. Alguém clica em Curtir, o contador sobe na hora e a requisição roda em segundo plano.
Esse feedback instantâneo faz o app parecer rápido. Em redes lentas, muitas vezes é a diferença entre “rápido” e “vai ou não vai?”.
A troca é a deriva de dados: o que o usuário vê pode deixar de corresponder ao que é verdade no servidor. A deriva normalmente aparece como pequenas inconsistências frustrantes que dependem do tempo e são difíceis de reproduzir.
Os usuários notam deriva quando algo “muda de ideia” depois: um contador pula e volta, um item aparece e some após um refresh, uma edição parece ter sido salva até você voltar à página, ou duas abas mostram valores diferentes.
Isso acontece porque a UI fez um palpite e o servidor pode responder com uma verdade diferente. Regras de validação, deduplicação, checagens de permissão, limites de taxa ou outro dispositivo alterando o mesmo registro podem mudar o resultado final. Outra causa comum são requisições sobrepostas: uma resposta mais antiga chega por último e sobrescreve a ação mais recente do usuário.
Exemplo: você renomeia um projeto para “Q1 Plan” e mostra isso no cabeçalho imediatamente. O servidor pode aparar espaços, rejeitar certos caracteres ou gerar um slug. Se você não substituir o valor otimista pela versão final do servidor, a UI parece correta até o próximo refresh, quando ela “misteriosamente” muda.
UI otimista não é sempre a escolha certa. Tenha cuidado (ou evite) para dinheiro e faturamento, ações irreversíveis, mudanças de função e permissão, fluxos com regras server-side complexas ou qualquer coisa com efeitos colaterais que o usuário precise confirmar explicitamente.
Usada corretamente, a atualização otimista faz o app parecer imediato — mas só se você planejar reconciliação, ordenação e tratamento de falhas.
UI otimista funciona melhor quando você separa dois tipos de estado:
A maior parte da deriva começa quando um palpite local é tratado como verdade confirmada.
Uma regra simples: se um valor tem significado de negócio fora da tela atual, o servidor é a fonte da verdade. Se afeta apenas o comportamento da tela (aberto/fechado, input com foco, texto rascunho), mantenha-o local.
Na prática, mantenha a verdade do servidor para coisas como permissões, preços, saldos, inventário, campos computados ou validados e qualquer coisa que possa mudar em outro lugar (outra aba, outro usuário). Mantenha estado local para rascunhos, flags de “está editando”, filtros temporários, linhas expandidas e toggles de animação.
Algumas ações são “seguras para adivinhar” porque o servidor quase sempre as aceita e são fáceis de reverter, como favoritar um item ou alternar uma preferência simples.
Quando um campo não é seguro para adivinhar, você ainda pode fazer o app parecer rápido sem fingir que a mudança é final. Mantenha o último valor confirmado e adicione um sinal claro de pendência.
Por exemplo, numa tela de CRM em que você clica em “Marcar como pago”, o servidor pode rejeitar (permissões, validação, já reembolsado). Em vez de reescrever imediatamente todos os números derivados, atualize o status com um rótulo sutil “Salvando…”, mantenha os totais inalterados e atualize-os só depois da confirmação.
Padrões bons são simples e consistentes: um pequeno badge “Salvando…” perto do item alterado, desabilitar temporariamente a ação (ou transformá-la em Desfazer) até a requisição terminar, ou marcar visualmente o valor otimista como temporário (texto mais claro ou um spinner pequeno).
Se a resposta do servidor pode afetar muitos lugares (totais, ordenação, campos computados, permissões), refazer o fetch geralmente é mais seguro do que tentar consertar tudo localmente. Se for uma mudança pequena e isolada (renomear uma nota, alternar uma flag), patchar localmente costuma ser suficiente.
Uma regra útil: aplique patch no que o usuário mudou, depois refetch em qualquer dado que seja derivado, agregado ou compartilhado entre telas.
UI otimista funciona quando seu modelo de dados acompanha o que está confirmado versus o que é apenas um palpite. Se você modelar essa lacuna explicitamente, momentos de “por que isso voltou?” ficam raros.
Para itens recém-criados, atribua um ID cliente temporário (como temp_12345 ou um UUID) e troque pelo ID real do servidor quando a resposta chegar. Isso permite que listas, seleção e estado de edição reconcilem sem problemas.
Exemplo: um usuário adiciona uma tarefa. Você a renderiza imediatamente com id: "temp_a1". Quando o servidor responde com id: 981, você substitui o ID em um só lugar e qualquer coisa indexada por ID continua funcionando.
Uma flag de loading no nível da tela é muito grosseira. Acompanhe o status no item (ou até no campo) que está mudando. Assim você mostra UI de pendência sutil, tenta apenas o que falhou e evita bloquear ações não relacionadas.
Um formato prático de item:
id: real ou temporáriostatus: pending | confirmed | failedoptimisticPatch: o que você mudou localmente (pequeno e específico)serverValue: último dado confirmado (ou um confirmedAt timestamp)rollbackSnapshot: o valor confirmado anterior que você pode restaurarAtualizações otimistas são mais seguras quando você toca somente no que o usuário realmente mudou (por exemplo, alternar completed) em vez de substituir o objeto todo por uma “nova versão” chutada. Substituir tudo facilita apagar edições mais recentes, campos adicionados pelo servidor ou mudanças concorrentes.
Uma boa atualização otimista parece instantânea, mas termina batendo com o que o servidor diz. Trate a mudança otimista como temporária e mantenha controle suficiente para confirmar ou desfazê-la com segurança.
Exemplo: um usuário edita o título de uma tarefa numa lista. Você quer que o título atualize na hora, mas também precisa lidar com erros de validação e formatação do servidor.
Aplique a mudança otimista imediatamente no estado local. Guarde um pequeno patch (ou snapshot) para poder reverter.
Envie a requisição com um request ID (um número incremental ou ID aleatório). É assim que você faz o match entre respostas e a ação que as disparou.
Marque o item como pendente. Pendente não precisa bloquear a UI. Pode ser um spinner pequeno, texto esmaecido ou “Salvando…”. O essencial é que o usuário entenda que ainda não está confirmado.
No sucesso, substitua os dados temporários do cliente pela versão do servidor. Se o servidor ajustou algo (aparou espaços, mudou casing, atualizou timestamps), atualize o estado local para bater com ele.
Na falha, reverta somente o que essa requisição mudou e mostre um erro local claro. Evite desfazer partes não relacionadas da tela.
Aqui está um formato pequeno que você pode seguir (agnóstico de biblioteca):
const requestId = crypto.randomUUID();
applyOptimistic({ id, title: nextTitle, pending: requestId });
try {
const serverItem = await api.updateTask({ id, title: nextTitle, requestId });
confirmSuccess({ id, requestId, serverItem });
} catch (err) {
rollback({ id, requestId });
showError("Could not save. Your change was undone.");
}
Dois detalhes previnem muitos bugs: armazene o request ID no item enquanto ele estiver pendente, e confirme ou faça rollback só se os IDs baterem. Isso evita que respostas antigas sobrescrevam edições mais novas.
UI otimista quebra quando a rede responde fora de ordem. Um erro clássico: o usuário edita um título, edita de novo logo em seguida, e a primeira requisição termina por último. Se você aplicar essa resposta tardia, a UI volta para um valor antigo.
A correção é tratar cada resposta como “talvez relevante” e aplicá-la apenas se corresponder à intenção mais recente do usuário.
Um padrão prático é um request ID cliente (um contador) anexado a cada mudança otimista. Armazene o último ID por registro. Quando uma resposta chegar, compare os IDs. Se a resposta for mais antiga que o último, ignore-a.
Checagens de versão também ajudam. Se o servidor retornar updatedAt, version ou um etag, aceite apenas respostas mais novas do que o que a UI já mostra.
Outras opções que você pode combinar:
Exemplo (guarda por request ID):
let nextId = 1;
const latestByItem = new Map();
async function saveTitle(itemId, title) {
const requestId = nextId++;
latestByItem.set(itemId, requestId);
// optimistic update
setItems(prev => prev.map(i => i.id === itemId ? { ...i, title } : i));
const res = await api.updateItem(itemId, { title, requestId });
// ignore stale response
if (latestByItem.get(itemId) !== requestId) return;
// reconcile with server truth
setItems(prev => prev.map(i => i.id === itemId ? { ...i, ...res.item } : i));
}
Se os usuários digitam rápido (notas, títulos, busca), considere cancelar ou atrasar saves até que façam uma pausa. Isso reduz carga no servidor e diminui a chance de respostas tardias causarem saltos visíveis.
Falhas são onde UI otimista pode perder confiança. A pior experiência é um rollback súbito sem explicação.
Um padrão bom para edições é: mantenha o valor do usuário na tela, marque como não salvo e mostre um erro inline exatamente onde ele editou. Se alguém renomeia um projeto de “Alpha” para “Q1 Launch”, não volte para “Alpha” a menos que seja necessário. Mantenha “Q1 Launch”, adicione “Não salvo. Nome já existe” e deixe o usuário corrigir.
Feedback inline fica preso ao campo ou linha que falhou. Evita o momento de “o que aconteceu?” onde um toast aparece e a UI muda de forma silenciosa.
Sinais confiáveis incluem “Salvando…” enquanto em voo, “Não salvo” na falha, um destaque sutil na linha afetada e uma mensagem curta que diga ao usuário o que fazer a seguir.
Retry é quase sempre útil. Undo é melhor para ações rápidas que alguém pode se arrepender (como arquivar), mas pode confundir em edições onde o usuário claramente quer o novo valor.
Quando uma mutação falha:
Se for preciso reverter (por exemplo, perda de permissão), explique e restaure a verdade do servidor: “Não foi possível salvar. Você não tem mais acesso para editar isto.”
Trate a resposta do servidor como o comprovante, não apenas uma flag de sucesso. Depois da requisição, reconciliem: mantenha o que o usuário quis dizer e aceite o que o servidor conhece melhor.
Um refetch completo é mais seguro quando o servidor pode ter mudado mais do que seu palpite local. Também é mais fácil de raciocinar.
Refetch costuma ser a melhor escolha quando a mutação afeta muitos registros (mover itens entre listas), quando permissões ou regras de workflow podem alterar o resultado, quando o servidor retorna dados parciais ou quando outros clientes atualizam a mesma visualização com frequência.
Se o servidor retornar a entidade atualizada (ou campos suficientes), o merge pode ser uma experiência melhor: a UI fica estável e ainda aceita a verdade do servidor.
A deriva frequentemente vem de sobrescrever campos controlados pelo servidor com um objeto otimista. Pense em contadores, valores computados, timestamps e formatação normalizada.
Exemplo: você define otimisticamente likedByMe=true e incrementa likeCount. O servidor pode deduplicar likes duplos e retornar um likeCount diferente, além de atualizar updatedAt.
Uma abordagem simples de merge:
Quando há conflito, decida antecipadamente. “Última escrita vence” funciona para toggles. Merge por campo é melhor para formulários.
Rastrear uma flag por campo “dirty since request” (ou um número de versão local) deixa você ignorar valores do servidor para campos que o usuário mudou após o início da mutação, enquanto aceita a verdade do servidor para o resto.
Se o servidor rejeitar a mutação, prefira erros específicos e leves a um rollback-surpresa. Mantenha a entrada do usuário, destaque o campo e mostre a mensagem. Rollbacks completos ficam para casos em que a ação realmente não pode se manter (por exemplo, você removeu otimisticamente um item que o servidor se recusou a deletar).
Listas são onde UI otimista é ótima e também onde quebra fácil. Um item que muda pode afetar ordenação, totais, filtros e várias páginas.
Para creates, mostre o novo item imediatamente, mas marque-o como pendente com um ID temporário. Mantenha sua posição estável para que não pule.
Para deletes, um padrão seguro é esconder o item imediatamente, mas manter um registro “fantasma” por curto período em memória até o servidor confirmar. Isso suporta undo e facilita lidar com falhas.
Reordenar é complicado porque toca muitos itens. Se você reordenar otimisticamente, guarde a ordem anterior para poder restaurá-la se necessário.
Com paginação ou infinite scroll, decida onde inserir otimisticamente. Em feeds, novos itens geralmente vão para o topo. Em catálogos ordenados pelo servidor, inserções locais podem enganar porque o servidor pode posicionar o item em outro lugar. Um compromisso prático é inserir na lista visível com um badge de pendente e estar pronto para mover depois da resposta se a chave de ordenação final for diferente.
Quando um ID temporário vira um ID real, faça dedupe por uma chave estável. Se você só casar por ID, pode acabar mostrando o mesmo item duas vezes (temp e confirmado). Mantenha um mapeamento tempId->realId e substitua no lugar para que posição de scroll e seleção não sejam resetadas.
Contagens e filtros também são estado de lista. Atualize contagens otimisticamente só quando tiver confiança de que o servidor concordará. Caso contrário, marque como atualizando e reconciliue após a resposta.
A maioria dos bugs de atualização otimista não é sobre React. Vêm de tratar a mudança otimista como “a nova verdade” em vez de um palpite temporário.
Atualizar otimisticamente um objeto inteiro ou a tela inteira quando só um campo mudou amplia a área de impacto. Correções do servidor depois podem sobrescrever edições não relacionadas.
Exemplo: um formulário de perfil substitui todo o objeto user quando você alterna uma configuração. Enquanto a requisição está em voo, o usuário edita o nome. Quando a resposta chega, sua substituição pode restaurar o nome antigo.
Mantenha patches otimistas pequenos e focados.
Outra fonte de deriva é esquecer de limpar flags de pendente após sucesso ou erro. A UI fica meio carregando e lógica posterior pode tratá-la como ainda otimista.
Se você rastrear estado pendente por item, limpe-o usando a mesma chave com que foi definido. IDs temporários frequentemente causam itens “fantasma pendentes” quando o ID real não é mapeado em todos os lugares.
Bugs de rollback acontecem quando o snapshot é salvo tarde demais ou com escopo amplo demais.
Se um usuário faz duas edições rápidas, você pode acabar revertendo a edição #2 com o snapshot anterior à edição #1. A UI pula para um estado que o usuário nunca viu.
Correção: capture o snapshot do recorte exato que você vai restaurar e vincule-o a uma tentativa de mutação específica (geralmente usando o request ID).
Saves reais costumam ter múltiplas etapas. Se a etapa 2 falhar (por exemplo, upload de imagem), não anule silenciosamente a etapa 1. Mostre o que salvou, o que não salvou e o que o usuário pode fazer em seguida.
Também não presuma que o servidor ecoará exatamente o que você enviou. Servidores normalizam texto, aplicam permissões, setam timestamps, atribuem IDs e descartam campos. Sempre reconcilie pela resposta (ou refetch) em vez de confiar no patch otimista para sempre.
UI otimista funciona quando é previsível. Trate cada mudança otimista como uma mini-transação: tem um ID, um estado pendente visível, uma troca clara no sucesso e um caminho de falha que não surpreende as pessoas.
Checklist para revisar antes de enviar:
Se você está prototipando rápido, mantenha a primeira versão pequena: uma tela, uma mutação, uma atualização de lista. Ferramentas como Koder.ai (koder.ai) podem ajudar a esboçar a UI e a API mais rápido, mas a mesma regra se aplica: modele pendente vs confirmado para que o cliente nunca perca o controle do que o servidor aceitou.
Optimistic UI atualiza a tela imediatamente, antes do servidor confirmar a mudança. Isso deixa o app com sensação instantânea, mas você ainda precisa reconciliar com a resposta do servidor para que a interface não se desalinhe do estado realmente salvo.
A deriva de dados ocorre quando a UI mantém uma suposição otimista como se fosse confirmada, mas o servidor salva algo diferente ou rejeita a operação. Geralmente aparece após um refresh, em outra aba ou quando respostas chegam fora de ordem por causa de redes lentas.
Evite ou seja muito cauteloso com atualizações otimistas em casos que envolvem dinheiro, faturamento, ações irreversíveis, mudanças de permissão e fluxos com regras server-side complexas. Nesses cenários é mais seguro mostrar um estado pendente e esperar pela confirmação antes de atualizar valores que afetam totais ou acessos.
Trate o backend como fonte da verdade para qualquer coisa que tenha significado de negócio fora da tela atual, como preços, permissões, campos computados e contadores compartilhados. Mantenha estado local para rascunhos, foco, flags de "está editando", filtros e outros estados puramente de apresentação.
Mostre um sinal pequeno e consistente exatamente onde a mudança ocorreu, como “Saving…”, texto esmaecido ou um spinner discreto. O objetivo é deixar claro que o valor é temporário sem bloquear toda a página.
Use um ID cliente temporário (como um UUID ou temp_...) ao criar o item e substitua pelo ID real do servidor quando a operação for bem-sucedida. Isso mantém as chaves da lista, seleção e estado de edição estáveis para evitar flicker ou duplicação.
Não use uma flag global de loading; acompanhe o estado pendente por item (ou por campo) para que apenas o elemento alterado apareça como pendente. Armazene um pequeno patch otimista e um snapshot de rollback para confirmar ou reverter só aquela mudança sem impactar a UI não relacionada.
Anexe um request ID a cada mutação e armazene o último request ID por item. Quando uma resposta chegar, aplique-a somente se corresponder ao último ID; caso contrário, ignore-a para que respostas tardias não façam a interface voltar a um valor antigo.
Para a maioria das edições, mantenha o valor do usuário visível, marque como não salvo e mostre um erro inline onde ele editou, com uma opção clara de Retry. Faça rollback rígido apenas quando a mudança realmente não puder ser mantida (por exemplo, perda de permissão) e explique o motivo.
Faça refetch quando a mudança puder afetar muitos lugares (totais, ordenação, permissões ou campos derivados), porque tentar corrigir tudo manualmente dá margem a erros. Faça merge local quando for uma atualização pequena e isolada e o servidor retornar a entidade atualizada; então limpe o estado pendente e aceite campos controlados pelo servidor como timestamps e valores computados.