Gerenciar estado é difícil porque apps lidam com muitas fontes de verdade, dados assíncronos, interações de UI e trade-offs de desempenho. Aprenda padrões para reduzir bugs.

Em um app frontend, estado é simplesmente os dados dos quais sua UI depende e que podem mudar ao longo do tempo.
Quando o estado muda, a tela deve atualizar para corresponder. Se a tela não atualiza, atualiza de forma inconsistente ou mostra uma mistura de valores antigos e novos, você sente “problemas de estado” imediatamente — botões que ficam desabilitados, totais que não batem ou uma visualização que não reflete o que o usuário acabou de fazer.
O estado aparece em interações pequenas e grandes, como:
Alguns desses são “temporários” (como uma aba selecionada), enquanto outros parecem “importantes” (como um carrinho). Todos são estado porque influenciam o que a UI renderiza agora.
Uma variável simples só importa onde ela vive. Estado é diferente porque tem regras:
O objetivo real do gerenciamento de estado não é armazenar dados — é tornar as atualizações previsíveis para que a UI permaneça consistente. Quando você consegue responder “o que mudou, quando e por quê”, o estado vira algo administrável. Quando não consegue, até recursos simples viram surpresas.
No início de um projeto frontend, o estado parece quase entediante — de um jeito bom. Você tem um componente, um input e uma atualização óbvia. Um usuário digita, você salva o valor e a UI re-renderiza. Tudo é visível, imediato e contido.
Imagine um único input de texto que mostra um preview do que você digitou:
Nesse cenário, estado é basicamente: uma variável que muda ao longo do tempo. Você aponta onde ela está armazenada e onde é atualizada, e pronto.
O estado local funciona porque o modelo mental casa com a estrutura do código:
Mesmo usando um framework como React, você não precisa pensar profundamente sobre arquitetura. Os padrões padrões são suficientes.
Assim que o app deixa de ser “uma página com um widget” e vira “um produto”, o estado para de viver em um só lugar.
Agora o mesmo dado pode ser necessário em:
Um nome de perfil pode aparecer em um cabeçalho, ser editado numa página de configurações, ficar em cache para carregamento mais rápido e ainda personalizar uma mensagem de boas-vindas. De repente, a questão não é “onde armazeno este valor?” mas “onde este valor deve ficar para permanecer correto em todo lugar?”.
A complexidade do estado não aumenta gradualmente com recursos — ela pula.
Adicionar um segundo lugar que lê o mesmo dado não é “duas vezes mais difícil”. Introduz problemas de coordenação: manter as views consistentes, evitar valores obsoletos, decidir o que atualiza o quê e lidar com o tempo. Quando você tem alguns pedaços de estado compartilhado mais trabalho assíncrono, pode acabar com comportamentos difíceis de raciocinar — mesmo que cada recurso individual pareça simples.
O estado fica doloroso quando o mesmo “fato” é armazenado em mais de um lugar. Cada cópia pode divergir e sua UI começa a discutir consigo mesma.
A maioria dos apps acaba com vários lugares que podem conter a “verdade”:
Todos esses são donos válidos de algum estado. O problema começa quando todos tentam ser donos do mesmo estado.
Um padrão comum: buscar dados do servidor e então copiá-los para estado local “para editar”. Ex.: carregar um perfil e fazer formState = userFromApi. Mais tarde, o servidor refaz a requisição (ou outra aba atualiza o registro) e agora você tem duas versões: o cache diz uma coisa, seu formulário diz outra.
A duplicação também entra por transformações “úteis”: armazenar items e itemsCount, ou manter selectedId e selectedItem.
Quando existem múltiplas fontes de verdade, os bugs costumam soar como:
Para cada pedaço de estado, escolha um dono — o lugar onde as atualizações são feitas — e trate todo o resto como uma projeção (somente leitura, derivada ou sincronizada em uma direção). Se você não consegue apontar para o dono, provavelmente está armazenando a mesma verdade duas vezes.
Muito do estado no frontend parece simples porque é síncrono: o usuário clica, você seta um valor, a UI atualiza. Efeitos colaterais quebram essa história passo a passo.
Efeitos colaterais são ações que atingem além do modelo puro de “render baseado em dados” do componente:
Cada um pode disparar depois, falhar inesperadamente ou rodar mais de uma vez.
Atualizações assíncronas introduzem o tempo como variável. Você não raciocina mais sobre “o que aconteceu”, mas sobre “o que ainda pode estar acontecendo”. Duas requisições podem se sobrepor. Uma resposta lenta pode chegar depois de outra mais nova. Um componente pode desmontar enquanto um callback assíncrono ainda tenta atualizar o estado.
Por isso bugs frequentemente parecem com:
Em vez de espalhar booleanos como isLoading pela UI, trate trabalho assíncrono como uma pequena máquina de estados:
Acompanhe os dados e o status juntos, e mantenha um identificador (como um id de requisição ou chave de query) para que você possa ignorar respostas tardias. Isso torna “o que a UI deve mostrar agora?” uma decisão direta, não um palpite.
Muitos problemas de estado começam com uma confusão simples: tratar “o que o usuário está fazendo agora” igual a “o que o backend diz que é verdade”. Ambos podem mudar com o tempo, mas seguem regras diferentes.
Estado da UI é temporário e guiado pela interação. Existe para renderizar a tela do jeito que o usuário espera neste momento.
Exemplos: modais abertos/fechados, filtros ativos, rascunho de um input de busca, hover/focus, aba selecionada e UI de paginação (página atual, tamanho da página, posição de scroll).
Esse estado é normalmente local a uma página ou árvore de componentes. Tudo bem se resetar ao navegar embora.
Estado do servidor é dado de uma API: perfis de usuário, listas de produtos, permissões, notificações, configurações salvas. É a “verdade remota” que pode mudar sem que sua UI faça nada (alguém edita, o servidor recalcula, um job em background atualiza).
Por ser remoto, também precisa de metadados: estados de carregamento/erro, timestamps de cache, retries e invalidação.
Se você armazena rascunhos de UI dentro de dados do servidor, um refetch pode apagar edições locais. Se armazenar respostas do servidor em estado de UI sem regras de cache, vai brigar com dados obsoletos, fetches duplicados e telas inconsistentes.
Um modo comum de falha: o usuário edita um formulário enquanto um refetch em background termina, e a resposta que chega sobrescreve o rascunho.
Gerencie estado do servidor com padrões de cache (fetch, cache, invalidar, refetch ao focar) e trate-o como compartilhado e assíncrono.
Gerencie o estado da UI com ferramentas de UI (estado local de componente, context para preocupações de UI realmente compartilhadas) e mantenha rascunhos separados até que você intencionalmente os “salve” de volta no servidor.
Estado derivado é qualquer valor que você pode computar a partir de outro estado: total do carrinho a partir dos itens, lista filtrada a partir da lista original + texto de busca, ou um flag canSubmit a partir dos valores dos campos e regras de validação.
É tentador armazenar esses valores porque parece conveniente (“vou guardar total também”). Mas assim que as entradas mudam em mais de um lugar, você corre risco de divergência: o total armazenado não bate com os itens, a lista filtrada não reflete a query atual ou o botão de enviar fica desabilitado depois de consertar um erro. Esses bugs irritam porque nada parece “errado” isoladamente — cada variável de estado é válida por si, só que inconsistente com o resto.
Um padrão mais seguro é: armazene a mínima fonte de verdade e compute o resto na leitura. Em React isso pode ser uma função simples ou um cálculo memoizado.
const items = useCartItems();
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const filtered = products.filter(p => p.name.includes(query));
Em apps maiores, “seletores” (ou getters computados) formalizam essa ideia: um lugar define como derivar total, filteredProducts, visibleTodos, e todos os componentes usam a mesma lógica.
Calcular em cada render geralmente é ok. Faça cache quando você mediu um custo real: transformações caras, listas enormes ou valores derivados compartilhados por muitos componentes. Use memoização (useMemo, memoização de seletores) para que as chaves do cache sejam as entradas verdadeiras — caso contrário você volta ao problema de divergência, só que com uma desculpa de desempenho.
O estado fica doloroso quando não está claro quem possui ele.
O dono de um pedaço de estado é o lugar no app que tem o direito de atualizá-lo. Outras partes da UI podem ler (via props, context, seletores, etc.), mas não devem mudá-lo diretamente.
Uma propriedade clara responde duas perguntas:
Quando essas fronteiras ficam borradas, você tem atualizações conflitantes, momentos de “por que isso mudou?” e componentes difíceis de reutilizar.
Colocar estado em um store global (ou context de topo) pode parecer limpo: qualquer um acessa e você evita prop drilling. A troca é acoplamento não intencional — telas não relacionadas passam a depender dos mesmos valores, e pequenas mudanças se espalham pelo app.
Estado global é bom para coisas realmente cross-cutting, como sessão do usuário, feature flags globais ou uma fila de notificações compartilhada.
Um padrão comum é começar local e “elevar” o estado para o pai comum mais próximo apenas quando dois irmãos precisam se coordenar.
Se somente um componente precisa do estado, mantenha-o lá. Se vários componentes precisam, eleve para o menor dono compartilhado. Se muitas áreas distantes precisam, só então considere global.
Mantenha o estado perto de onde é usado, a menos que o compartilhamento seja realmente necessário.
Isso mantém os componentes mais fáceis de entender, reduz dependências acidentais e torna refatores futuros menos assustadores porque menos partes do app podem mutar os mesmos dados.
Apps frontend parecem “single-threaded”, mas entrada do usuário, timers, animações e requisições de rede rodam de forma independente. Isso significa que múltiplas atualizações podem estar em voo ao mesmo tempo — e nem sempre terminam na ordem em que você as iniciou.
Uma colisão comum: duas partes da UI atualizam o mesmo estado.
query a cada tecla.query (ou a mesma lista de resultados) quando mudado.Isoladamente, cada atualização está correta. Juntas, podem sobrescrever uma à outra dependendo do timing. Pior ainda, você pode acabar mostrando resultados de uma query anterior enquanto a UI exibe novos filtros.
Condições de corrida aparecem quando você dispara requisição A e, rapidamente, dispara B — mas A retorna depois.
Exemplo: o usuário digita “c”, “ca”, “cat”. Se a requisição de “c” for lenta e a de “cat” rápida, a UI pode mostrar resultados de “cat” e, em seguida, ser sobrescrita por resultados obsoletos de “c” quando essa resposta atrasada chegar.
O bug é sutil porque tudo “funcionou” — só que na ordem errada.
Geralmente você quer uma destas estratégias:
AbortController).Uma abordagem simples com request ID:
let latestRequestId = 0;
async function fetchResults(query) {
const requestId = ++latestRequestId;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
if (requestId !== latestRequestId) return; // resposta obsoleta
setResults(data);
}
Atualizações otimistas fazem a UI parecer imediata: você atualiza a tela antes do servidor confirmar. Mas concorrência pode quebrar suposições:
Para tornar o otimista seguro, normalmente você precisa de uma regra clara de reconciliação: rastrear a ação pendente, aplicar respostas do servidor em ordem e, se precisar fazer rollback, voltar a um checkpoint conhecido (não “o que a UI parece agora”).
Atualizações de estado não são “de graça”. Quando o estado muda, o app precisa decidir quais partes da tela podem ser afetadas e fazer o trabalho para refletir a nova realidade: recalcular valores, re-renderizar UI, reexecutar lógica de formatação e, às vezes, re-buscar ou re-validar dados. Se essa reação em cadeia for maior do que precisa, o usuário sente como lag, travamento ou botões que parecem “pensar” antes de responder.
Um toggle simples pode acidentalmente disparar muito trabalho extra:
O resultado não é só técnico — é experiencial: digitar fica lento, animações engasgam e a interface perde a sensação “snappy” que usuários associam a produtos polidos.
Uma das causas mais comuns é estado que é amplo demais: um objeto “big bucket” contendo muita informação não relacionada. Atualizar qualquer campo faz o bucket inteiro parecer novo, então mais UI acorda do que o necessário.
Outra armadilha é armazenar valores computados em estado e atualizá-los manualmente. Isso frequentemente cria atualizações extras (e mais trabalho de UI) só para manter tudo em sincronia.
Divida o estado em fatias menores. Mantenha preocupações não relacionadas separadas para que mudar um input de busca não atualize uma página inteira de resultados.
Normalize dados. Em vez de armazenar o mesmo item em muitos lugares, armazene-o uma vez e referencie-o. Isso reduz atualizações repetidas e evita “tempestades de mudança” onde uma edição força muitas cópias a serem reescritas.
Memoize valores derivados. Se um valor pode ser calculado a partir de outro estado (como resultados filtrados), faça cache desse cálculo para que só seja refeito quando as entradas realmente mudarem.
Gerenciamento de estado com foco em desempenho é em grande parte sobre contenção: atualizações devem afetar a menor área possível e trabalho caro deve acontecer somente quando realmente necessário. Quando isso acontece, usuários param de notar o framework e começam a confiar na interface.
Bugs de estado frequentemente parecem pessoais: a UI está “errada”, mas você não consegue responder à pergunta mais simples — quem mudou este valor e quando? Se um número vira, um banner some ou um botão se desabilita, você precisa de uma linha do tempo, não de um palpite.
O caminho mais rápido para clareza é um fluxo de atualização previsível. Seja usando reducers, eventos ou um store, busque um padrão onde:
setShippingMethod('express'), não updateStuff)Logs claros de ações transformam depuração de “encarar a tela” em “seguir o recibo”. Mesmo console logs simples (nome da ação + campos-chave) vencem tentar reconstruir o que aconteceu a partir de sintomas.
Não tente testar cada re-render. Em vez disso, teste as partes que devem se comportar como lógica pura:
Essa mistura pega tanto “bugs de cálculo” quanto problemas de ligação no mundo real.
Problemas assíncronos se escondem nas lacunas. Adicione metadados mínimos que tornem visíveis as linhas do tempo:
Então, quando uma resposta tardia sobrescrever uma mais nova, você prova isso imediatamente — e corrige com confiança.
Escolher uma ferramenta de estado é mais fácil quando você a trata como resultado de decisões de design, não como ponto de partida. Antes de comparar bibliotecas, mapeie suas fronteiras de estado: o que é puramente local ao componente, o que precisa ser compartilhado e o que é realmente “dados do servidor” que você busca e sincroniza.
Uma maneira prática de decidir é olhar para algumas restrições:
Se você começar com “usamos X em todo lugar”, vai acabar armazenando as coisas erradas nos lugares errados. Comece com propriedade: quem atualiza este valor, quem o lê e o que deve acontecer quando mudar.
Muitos apps vão bem com uma biblioteca de server-state para dados da API e uma solução pequena para estado de UI para preocupações client-only como modais, filtros ou rascunhos de formulário. O objetivo é clareza: cada tipo de estado vive onde é mais fácil de raciocinar.
Se você está iterando sobre fronteiras de estado e fluxos assíncronos, Koder.ai pode acelerar o loop de “tentar, observar, refinar”. Como gera frontends React (e backends Go + PostgreSQL) a partir de chat com um workflow baseado em agentes, você pode prototipar modelos de propriedade alternativos (local vs global, cache do servidor vs rascunhos de UI) rapidamente e manter a versão que se mantém previsível.
Duas funcionalidades práticas ajudam ao experimentar estado: Modo de Planejamento (para esboçar o modelo de estado antes de construir) e instantâneos + rollback (para testar refactors como “remover estado derivado” ou “introduzir IDs de requisição” sem perder uma base funcional).
O estado fica mais fácil quando você o trata como um problema de design: decida quem o possui, o que ele representa e como muda. Use este checklist quando um componente começar a ficar “misterioso”.
Pergunte: Qual parte do app é responsável por esse dado? Coloque o estado o mais perto possível de onde é usado e eleve-o apenas quando múltiplas partes realmente precisarem.
Se você pode computar algo a partir de outro estado, não o armazene.
items, filterText).visibleItems) durante o render ou via memoização.Trabalho assíncrono é mais claro quando você o modela diretamente:
status: 'idle' | 'loading' | 'success' | 'error', além de data e error.isLoading, isFetching, isSaving, hasLoaded, …) em vez de um único status.Busque menos bugs do tipo “como isso entrou nesse estado?”, mudanças que não exigem tocar cinco arquivos e um modelo mental onde você aponta para um lugar e diz: aqui é onde mora a verdade.