A Ilusão da Latência Zero: Optimistic Updates no Next.js

Arquitetura Moderna para CRUDs no Next.js
- Parte 1: Arquitetando um CRUD Escalável
- Parte 2: Construindo uma Camada Profissional de Comunicação
- Parte 3: Gerenciando Estado Remoto com TanStack Query
- Parte 4: Formulários Escaláveis com React Hook Form + Zod
- Parte 5: Search Params, Filtros e Paginação
- 👉 Parte 6: UX Moderna: Optimistic Updates e Performance (Você está aqui)
Até aqui resolvemos a engenharia.
Construímos uma infraestrutura sólida, protegemos as mutações com o Zod, sincronizamos a navegação com Search Params e delegamos o gerenciamento do estado remoto ao TanStack Query.
Nossa aplicação é segura, manutenível e escalável.
Mas, para o usuário final, nada disso importa se o sistema parecer lento.
Neste último capítulo da série, vamos deixar de olhar apenas para a arquitetura do código e começar a olhar para a percepção de performance.
O Problema da Latência Inevitável
Imagine que o nosso CRUD de usuários já está em produção.
Um administrador encontra um usuário spammer na tabela e clica no botão "Deletar".
O que acontece em uma implementação tradicional?
- O usuário clica no botão.
- O botão exibe um spinner de carregamento.
- A requisição viaja pela rede, chega ao servidor, exclui o registro no banco de dados e retorna um
200 OK. - O TanStack Query invalida a
queryKeye busca novamente a lista de usuários. - O spinner desaparece e o usuário some da tabela.
Mesmo com uma infraestrutura excelente, sempre existirá a latência da rede.
Ela faz parte da comunicação entre cliente e servidor e simplesmente não pode ser eliminada.
Em uma conexão rápida, esse processo pode levar algumas centenas de milissegundos.
Em uma conexão instável, como uma rede móvel, pode levar vários segundos.
Para o cérebro humano, qualquer interação que demore mais de aproximadamente 100 ms para responder já reduz a sensação de instantaneidade.
É exatamente esse atraso que faz uma aplicação parecer lenta.
A Ilusão da Latência Zero (Optimistic Updates)
O que é um Optimistic Update (Atualização Otimista)?
É a técnica de assumir que a operação enviada ao servidor será concluída com sucesso.
Em vez de esperar a confirmação da API, antecipamos a atualização da interface imediatamente após a ação do usuário.
Enquanto isso, a requisição continua sendo executada normalmente em segundo plano.
Se tudo ocorrer como esperado, nenhuma alteração adicional é necessária.
Caso ocorra algum erro, desfazemos a alteração temporária e sincronizamos novamente a interface.
O resultado é uma experiência extremamente fluida.
Para o usuário, a aplicação transmite a sensação de que está executando todas as operações localmente.
É justamente esse tipo de detalhe que diferencia aplicações comuns de interfaces modernas.
O Servidor Continua Sendo a Fonte da Verdade
Existe um detalhe conceitual extremamente importante.
Um Optimistic Update não altera a responsabilidade do servidor.
O banco de dados continua sendo a fonte definitiva da verdade.
O que modificamos é apenas a representação temporária desses dados dentro do cache do TanStack Query.
Se a operação falhar, descartamos essa representação otimista e sincronizamos novamente a interface com a resposta real do servidor.
O fluxo arquitetural acontece exatamente assim:
Usuário clica em "Excluir"
↓
onMutate
↓
Cache do TanStack Query
(atualização otimista)
↓
Interface atualizada
↓
Server Action
↓
Service
↓
API
↓
───────────────────────────
Sucesso → invalidateQueries()
Erro → rollback do cache
Manipulando o Cache (A Prática)
Para implementar esse fluxo utilizamos três ciclos de vida fundamentais do useMutation.
onMutate
É executado no exato instante em que a mutação começa, antes mesmo da requisição sair do navegador.
É aqui que:
- cancelamos consultas em andamento;
- fazemos um backup do cache atual;
- aplicamos a atualização otimista.
onError
É executado somente quando a operação falha.
Nesse momento utilizamos o backup salvo anteriormente para restaurar o estado original da aplicação.
Esse processo é conhecido como rollback.
onSettled
É executado sempre ao final da mutação, independentemente de sucesso ou erro.
Seu objetivo é invalidar o cache para garantir que a interface reflita exatamente o estado armazenado no servidor.
A implementação fica assim:
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteUserAction } from "@/actions/users/delete-user-action";
import { User } from "@/types/user";
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteUserAction,
onMutate: async (userIdToDelete) => {
// 1. Cancela buscas em andamento
await queryClient.cancelQueries({
queryKey: ["users"],
});
// 2. Faz um backup do cache atual
const previousUsers =
queryClient.getQueryData(["users"]);
// 3. Atualiza o cache imediatamente
queryClient.setQueryData(
["users"],
(oldData: any) => {
if (!oldData) return oldData;
return {
...oldData,
items: oldData.items.filter(
(user: User) =>
user.id !== userIdToDelete
),
};
}
);
return {
previousUsers,
};
},
onError: (_, __, context) => {
queryClient.setQueryData(
["users"],
context?.previousUsers
);
alert(
"Falha ao deletar usuário. Tente novamente."
);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["users"],
});
},
});
}
Observe que não estamos modificando a tabela diretamente.
Também não criamos estados temporários utilizando useState.
Toda a alteração acontece dentro do cache do TanStack Query.
A interface simplesmente reage ao novo estado remoto.
Quando NÃO Utilizar Optimistic Updates
Atualizações otimistas são extremamente úteis, mas devem ser aplicadas com critério.
Situações recomendadas
- Exclusão de registros.
- Favoritar ou curtir conteúdos.
- Alteração de status.
- Movimentação de cartões em um Kanban.
- Alterações simples em registros existentes.
Situações em que devem ser evitadas
- Operações financeiras.
- Confirmação de pagamentos.
- Transferências bancárias.
- Criação de registros que dependem de um identificador definitivo gerado pelo servidor.
Nesses casos, criar um estado otimista pode gerar inconsistências difíceis de explicar para o usuário.
Uma regra prática costuma funcionar muito bem:
Se uma falha puder causar confusão significativa ou comprometer a confiança do usuário, prefira aguardar a confirmação do servidor.
O Ponto de Chegada
Ao longo desta série adicionamos uma camada de arquitetura por vez.
Primeiro organizamos a comunicação através de Services e Server Actions.
Depois centralizamos o estado remoto utilizando TanStack Query.
Em seguida estruturamos os formulários e os contratos de dados com React Hook Form e Zod.
Transformamos a URL em parte da arquitetura utilizando Search Params.
Por fim, utilizamos toda essa base para entregar uma experiência de usuário extremamente fluida através dos Optimistic Updates.
Nenhuma dessas camadas substituiu a anterior.
Cada uma resolveu exatamente um problema específico, comunicando-se de forma previsível com as demais.
Essa é justamente a essência de uma arquitetura escalável.
Ao final destes seis capítulos, não construímos apenas um CRUD.
Construímos uma arquitetura onde cada camada possui uma única responsabilidade, permitindo que a aplicação evolua de forma previsível, testável e sustentável.
Construir software não é empilhar bibliotecas populares.
É entender onde cada responsabilidade começa e termina, para que a aplicação possa crescer, mudar e evoluir sem que a base precise ser reconstruída.