Estado no frontend: server state, client state e cache de dados
Pergunte a qualquer time de frontend qual é a parte mais difícil de manter em uma aplicação, e a resposta quase sempre é a mesma: o estado. Não a interface, não o estilo, mas o gerenciamento dos dados que fluem pela tela. Bugs de estado são os mais escorregadios: uma lista que não atualiza, um dado velho que aparece, dois componentes que discordam sobre a mesma informação. Boa parte dessa dor vem de um erro conceitual, tratar todo estado como se fosse a mesma coisa. Não é.
A virada de chave que simplifica quase tudo é entender que existem dois tipos de estado com naturezas opostas: o server state e o client state. Confundi-los é a raiz de metade dos problemas. Este guia explica a diferença, mostra como cada um deve ser tratado e por que as ferramentas certas tornam o código mais simples, não mais complexo.
Dois tipos de estado, duas naturezas
A distinção é fundamental e vale internalizar:
- Client state é o estado que pertence à interface e vive só no navegador. Um menu aberto ou fechado, o texto que está sendo digitado num campo, a aba selecionada, o tema claro ou escuro. Ele é síncrono, você o controla totalmente, e ninguém mais o altera.
- Server state é o estado que pertence ao servidor e você apenas exibe uma cópia. A lista de produtos, o perfil do usuário, os pedidos. Ele é assíncrono, pode mudar sem você saber (outro usuário editou), pode ficar desatualizado, e você não é o dono, só um espelho temporário.
A tabela resume por que tratá-los igual dá errado:
| Característica | Client state | Server state |
|---|---|---|
| Dono da verdade | O próprio frontend | O servidor |
| Natureza | Síncrona | Assíncrona |
| Pode ficar velho? | Não | Sim, o tempo todo |
| Precisa de cache? | Não | Sim |
| Precisa de loading e erro? | Raramente | Sempre |
| Exemplo | Modal aberto, filtro de UI | Lista de usuários, saldo |
O ponto central: server state não é realmente estado, é cache. Você está guardando uma cópia local de algo que vive noutro lugar, e todo o desafio é manter essa cópia razoavelmente fresca. Ferramentas tradicionais de estado global foram feitas para client state e fazem um trabalho ruim com server state, porque não têm noção de cache, revalidação ou dados velhos.
Server state: o problema é cache
Tratar dados do servidor como um problema de cache muda tudo. E cache tem desafios conhecidos: quando buscar, quanto tempo confiar no que se tem, quando descartar. As bibliotecas modernas de data fetching (as chamadas query libraries) resolvem isso e entregam de graça o que antes se reimplementava mal em cada tela:
- Cache automático por chave. Cada requisição é identificada por uma chave (por exemplo,
['usuario', id]). Dois componentes que pedem o mesmo dado compartilham o mesmo cache, sem duas chamadas à rede. - Estados de carregamento e erro já embutidos, em vez de gerenciar manualmente flags de
loadingeerrorespalhadas. - Revalidação inteligente, buscando dados novos ao voltar para a aba, ao reconectar ou após um tempo definido.
- Deduplicação de requisições simultâneas para o mesmo recurso.
// buscar dados do servidor como cache, não como estado manual
const { data, isLoading, error } = useQuery({
queryKey: ['pedidos', usuarioId],
queryFn: () => api.get(`/usuarios/${usuarioId}/pedidos`),
staleTime: 60_000, // considera fresco por 60s antes de revalidar
});
Invalidação e stale-while-revalidate
Duas ideias organizam a frescura do cache. A invalidação é o ato de marcar um dado como desatualizado, forçando uma nova busca. O caso clássico: depois de criar um pedido, você invalida a chave ['pedidos'], e a lista se atualiza sozinha para incluir o novo item. Em vez de atualizar a lista na mão, você declara "esses dados mudaram" e a biblioteca cuida do resto.
O padrão stale-while-revalidate é o que torna a experiência fluida. A ideia: quando o usuário volta a uma tela, mostre imediatamente o dado que está no cache (mesmo que possa estar levemente velho) e, em segundo plano, busque a versão fresca e atualize a tela quando ela chegar. O usuário nunca fica olhando um spinner para dados que já foram carregados antes; ele vê algo na hora e a atualização acontece de forma quase invisível. É rápido e correto ao mesmo tempo.
Optimistic updates
Para ações de escrita, há uma técnica que faz a interface parecer instantânea: os optimistic updates (atualizações otimistas). Em vez de esperar o servidor confirmar para só então atualizar a tela, você atualiza a interface imediatamente, assumindo que a operação vai dar certo, e reconcilia depois.
Pense em curtir uma publicação: o coração fica vermelho na hora, sem esperar a resposta da rede. Nos bastidores, a requisição segue; se o servidor confirmar, nada muda; se falhar, a interface reverte para o estado anterior e avisa. A regra de uso:
- Use em ações de alta probabilidade de sucesso e baixo custo de erro (curtir, marcar como lido, reordenar).
- Sempre tenha um caminho de reversão para quando a operação falhar, devolvendo o estado anterior sem deixar a tela inconsistente.
- Evite em operações críticas onde mostrar sucesso antes da confirmação seria perigoso (um pagamento, por exemplo).
Client state: mantenha simples
Separado o server state, o que sobra de client state é surpreendentemente pouco e, na maioria das vezes, local. Um modal aberto, o valor de um input, uma aba ativa: isso vive no componente que o usa, com o estado local que o próprio framework oferece. Não precisa de biblioteca nenhuma.
A hierarquia de decisão para client state é simples:
- Estado local do componente para o que só ele usa. Comece sempre aqui.
- Elevar o estado ao ancestral comum quando dois componentes irmãos precisam compartilhá-lo.
- Contexto para dados globais de UI que muitos componentes leem mas que mudam pouco (tema, idioma, usuário logado).
- Biblioteca de estado global apenas para client state genuinamente complexo e compartilhado por toda a aplicação.
Quando o Redux é demais
Por anos, a resposta reflexa para "como gerencio estado?" foi "instale Redux". O resultado foram aplicações inteiras com centenas de linhas de boilerplate para guardar dados que eram, na verdade, server state mal tratado como estado global. Colocar a lista de usuários num store global, com actions e reducers, é reinventar (pior) um cache que uma query library entrega pronto.
A regra prática para 2026:
- Server state? Use uma query library. Não coloque isso num store global.
- Client state local? Estado do componente.
- Client state global e simples? Contexto ou uma biblioteca leve de estado.
- Client state global e realmente complexo? Aí sim uma solução de estado mais robusta pode se justificar, e mesmo assim as opções modernas são bem mais enxutas que o Redux clássico.
Na prática, quando você separa corretamente server de client state, a quantidade de estado global "de verdade" que sobra é tão pequena que ferramentas pesadas raramente se pagam. O Redux não está errado, ele só costuma ser a resposta para uma pergunta que, bem formulada, quase não existe.
Separe os tipos e o resto fica simples
A maior parte da complexidade de estado no frontend evapora quando você para de tratar tudo igual. Server state é cache: entregue-o a uma ferramenta que entende de chaves, invalidação e revalidação. Client state é da interface: mantenha-o o mais local e simples possível. Com essa separação, os bugs de dados velhos, listas dessincronizadas e spinners eternos diminuem drasticamente, e o código fica menor, não maior.
Na ConstructorTI, construímos frontends com uma arquitetura de estado deliberada: server state gerenciado como cache, com invalidação e stale-while-revalidate, client state enxuto e local, e optimistic updates onde eles melhoram a percepção de velocidade sem arriscar consistência. Se a sua aplicação sofre com bugs de estado, dados desatualizados ou um store global inchado, fale com o nosso time de engenharia e saia da conversa com um plano claro para simplificar a gestão de estado do seu produto.