Gerenciamento de estado em React simplificado: separe estado do servidor do estado do cliente, siga algumas regras e identifique cedo sinais de complexidade crescente.

Estado é qualquer dado que pode mudar enquanto seu app roda. Isso inclui o que você vê (um modal aberto), o que está editando (um rascunho de formulário) e dados que você busca (uma lista de projetos). O problema é que tudo isso acaba sendo chamado de estado, mesmo que tenha comportamentos bem diferentes.
A maioria dos apps bagunçados quebra do mesmo jeito: muitos tipos de estado se misturam no mesmo lugar. Um componente acaba mantendo dados do servidor, flags da UI, rascunhos de formulários e valores derivados, e tenta mantê-los alinhados com efeitos. Em pouco tempo, você não consegue responder perguntas simples como “de onde vem esse valor?” ou “o que o atualiza?” sem vasculhar vários arquivos.
Apps gerados tendem a entrar nessa situação mais rápido porque é fácil aceitar a primeira versão que funciona. Você adiciona uma nova tela, copia um padrão, corrige um bug com outro useEffect, e agora tem duas fontes de verdade. Se o gerador ou a equipe mudarem de direção no meio do caminho (estado local aqui, store global ali), a base de código coleciona padrões em vez de construir sobre um só.
O objetivo é chato: menos tipos de estado e menos lugares para procurar. Quando há um lar óbvio para dados do servidor e outro para estado só da UI, bugs ficam menores e mudanças deixam de parecer arriscadas.
"Mantenha chato" significa seguir algumas regras:
Um exemplo concreto: se uma lista de usuários vem do backend, trate-a como server state e busque onde for usada. Se selectedUserId existe só para controlar um painel de detalhes, mantenha como estado UI perto desse painel. Misturar os dois é o começo da complexidade.
A maioria dos problemas de estado em React começa com uma confusão: tratar dados do servidor como se fossem estado da UI. Separe cedo e o gerenciamento de estado fica tranquilo, mesmo com o app crescendo.
Server state pertence ao backend: usuários, pedidos, tarefas, permissões, preços, feature flags. Isso pode mudar sem o app fazer nada (outra aba atualiza, um admin edita, um job roda, dados expiram). Como é compartilhado e mutável, você precisa de fetch, cache, refetch e tratamento de erro.
Client state é o que só a sua UI se importa agora: qual modal está aberto, qual aba está selecionada, um toggle de filtro, ordem de ordenação, uma sidebar colapsada, um rascunho de busca. Se você fechar a aba, perder isso geralmente é aceitável.
Um teste rápido é: “Consigo atualizar a página e reconstruir isso a partir do servidor?”
Há também o estado derivado, que evita que você crie estado extra. É um valor que você pode calcular a partir de outros valores, então não armazena. Listas filtradas, totais, isFormValid e “mostrar empty state” normalmente ficam aqui.
Exemplo: você busca uma lista de projetos (server state). O filtro selecionado e a flag do diálogo “Novo projeto” são client state. A lista visível após filtrar é estado derivado. Se você armazenar a lista visível separadamente, ela vai sair de sincronia e você ficará caçando por que está desatualizada.
Essa separação ajuda quando uma ferramenta como Koder.ai gera telas rapidamente: mantenha dados do backend em uma camada de fetch, mantenha escolhas da UI perto dos componentes e evite armazenar valores calculados.
Estado vira dor quando um dado tem dois donos. A forma mais rápida de manter simples é decidir quem é dono do quê e manter isso.
Exemplo: você busca uma lista de usuários e mostra detalhes quando um é selecionado. Um erro comum é armazenar o objeto selectedUser completo no estado. Armazene selectedUserId em vez disso. Mantenha a lista no cache do servidor. A view de detalhes busca o usuário pelo ID, então refetches atualizam a UI sem código extra de sincronização.
Em apps React gerados, também é fácil aceitar estado “útil” gerado que duplica dados do servidor. Quando ver código que faz fetch -> setState -> editar -> refetch, pare. Isso frequentemente é um sinal de que você está construindo um segundo banco de dados no navegador.
Server state é tudo que vive no backend: listas, páginas de detalhe, resultados de busca, permissões, contagens. A abordagem chata é escolher uma ferramenta para isso e manter-se fiel. Para muitos apps React, TanStack Query é suficiente.
O objetivo é direto: componentes pedem dados, mostram loading e erro, e não se importam com quantas chamadas de fetch acontecem por baixo. Isso importa em apps gerados porque pequenas inconsistências se multiplicam conforme novas telas são adicionadas.
Trate chaves de query como um sistema de nomes, não como detalhe de última hora. Mantenha-as consistentes: chaves estáveis em arrays, inclua apenas entradas que mudam o resultado (filtros, página, ordenação) e prefira algumas formas previsíveis em vez de muitos casos isolados. Muitas equipes colocam a construção de chaves em pequenos helpers para que cada tela siga as mesmas regras.
Para escritas, use mutations com tratamento explícito de sucesso. Uma mutation deve responder duas perguntas: o que mudou e o que a UI deve fazer em seguida?
Exemplo: você cria uma nova tarefa. No sucesso, ou invalida a query da lista de tarefas (para rebaixar e recarregar) ou faz uma atualização direcionada no cache (adiciona a nova tarefa à lista em cache). Escolha uma abordagem por feature e mantenha-a consistente.
Se sentir vontade de adicionar refetches em vários lugares “só para garantir”, escolha um movimento chato único em vez disso:
Client state é aquilo que o navegador possui: uma flag de sidebar aberta, uma linha selecionada, texto de filtro, um rascunho antes de salvar. Mantenha perto de onde é usado e geralmente se mantém manejável.
Comece pequeno: useState no componente mais próximo. Ao gerar telas (por exemplo com Koder.ai), é tentador empurrar tudo para uma store global “por precaução”. É assim que você acaba com uma store que ninguém entende.
Só mova estado para cima quando você conseguir nomear o problema de compartilhamento.
Exemplo: uma tabela com um painel de detalhes pode manter selectedRowId no componente da tabela. Se uma toolbar em outra parte da página também precisar, eleve para o componente de página. Se outra rota (como edição em massa) precisar, aí sim uma store pequena faz sentido.
Se você usar uma store (Zustand ou similar), mantenha focada em uma única função. Armazene o “o quê” (IDs selecionados, filtros), não os “resultados” (listas ordenadas) que você pode derivar.
Quando uma store começar a crescer, pergunte: isso ainda é uma funcionalidade só? Se a resposta honesta for “meio que”, divida agora, antes que a próxima feature transforme numa bola de estado que você tem medo de mexer.
Bugs em formulários frequentemente vêm da mistura de três coisas: o que o usuário está digitando, o que o servidor salvou e o que a UI está mostrando.
Para gerenciamento de estado chato, trate o formulário como estado cliente até o envio. Dados do servidor são a última versão salva. O formulário é um rascunho. Não edite o objeto do servidor no lugar. Copie valores para o estado de rascunho, deixe o usuário alterar, então submeta e refaça o fetch (ou atualize o cache) no sucesso.
Decida cedo o que deve persistir quando o usuário navegar. Essa única escolha evita muitos bugs-surpresa. Por exemplo, modo de edição inline e dropdowns abertos normalmente devem resetar, enquanto um rascunho longo de um assistente ou uma mensagem não enviada pode persistir. Persista após reload só quando os usuários claramente esperam (como um checkout).
Mantenha regras de validação em um lugar só. Se espalhar regras por inputs, handlers de submit e helpers, você terá erros desencontrados. Prefira um esquema único (ou uma função validate()), e deixe a UI decidir quando mostrar erros (onChange, onBlur ou onSubmit).
Exemplo: você gera uma tela Edit Profile em Koder.ai. Carregue o perfil salvo como server state. Crie estado de rascunho para campos do formulário. Mostre “alterações não salvas” comparando rascunho vs salvo. Se o usuário cancelar, descarte o rascunho e mostre a versão do servidor. Se salvar, submeta o rascunho e então substitua a versão salva pela resposta do servidor.
À medida que um app gerado cresce, é comum acabar com os mesmos dados em três lugares: estado do componente, uma store global e um cache. A correção normalmente não é uma nova lib. É escolher um lar para cada pedaço de estado.
Um fluxo de limpeza que funciona na maioria dos apps:
filteredUsers se puder calcular a partir de users + filter. Prefira selectedUserId ao invés de um selectedUser duplicado.Exemplo: um app CRUD gerado por Koder.ai costuma começar com um useEffect de fetch mais uma cópia da lista numa store global. Depois de centralizar o server state, a lista vem de uma query, e “refresh” vira invalidação em vez de sincronização manual.
Para nomes, mantenha consistente e sem graça:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteO objetivo é uma fonte de verdade por coisa, com limites claros entre server state e client state.
Problemas de estado começam pequenos, então um dia você muda um campo e três partes da UI discordam sobre o valor “real”.
O sinal mais claro é dado: duplicação de dados — o mesmo usuário ou carrinho vive em um componente, numa store global e numa cache de requisição. Cada cópia atualiza em tempos diferentes, e você adiciona mais código só para mantê-las iguais.
Outro sinal é código de sincronização: efeitos que empurram estado de um lado pro outro. Padrões como “quando dados da query mudam, atualize a store” e “quando a store muda, refetch” podem funcionar até um caso de borda provocar valores stale ou loops.
Algumas bandeiras vermelhas rápidas:
needsRefresh, didInit, isSaving que ninguém remove.Exemplo: você gera um dashboard em Koder.ai e adiciona um modal Edit Profile. Se os dados do perfil estão em cache de query, copiados para uma store global e duplicados no estado local do formulário, agora você tem três fontes da verdade. Ao adicionar refetching em segundo plano ou updates otimistas, surgem divergências.
Quando vir esses sinais, o movimento chato é escolher um dono único para cada pedaço de dado e apagar os espelhos.
Guardar coisas “por precaução” é uma das formas mais rápidas de tornar o estado doloroso, especialmente em apps gerados.
Copiar respostas de API para uma store global é uma armadilha comum. Se os dados vêm do servidor (listas, detalhes, perfil do usuário), não copie para uma store cliente por padrão. Escolha um lar para dados do servidor (geralmente o cache de queries). Use a store cliente para valores só da UI que o servidor não conhece.
Armazenar valores derivados é outra armadilha. Contagens, listas filtradas, totais, canSubmit e isEmpty normalmente devem ser calculados a partir das entradas. Se o desempenho virar problema, memoize depois, mas não comece armazenando o resultado.
Uma mega-store única para tudo (auth, modais, toasts, filtros, rascunhos, flags de onboarding) vira depósito. Separe por limites de feature. Se o estado é usado por uma única tela, mantenha local.
Context é ótimo para valores estáveis (tema, id do usuário atual, locale). Para valores que mudam rápido, pode causar re-renders amplos. Use Context para wiring, e estado de componente (ou uma store pequena) para valores de UI que mudam com frequência.
Por fim, evite nomes inconsistentes. Chaves de query e campos de store quase iguais criam duplicação sutil. Escolha um padrão simples e siga-o.
Quando sentir vontade de adicionar “só mais uma” variável de estado, faça um checape de propriedade.
Primeiro, você consegue apontar um lugar onde fetching e caching do servidor acontecem (uma ferramenta de queries, um conjunto de chaves)? Se os mesmos dados são buscados em vários componentes e também copiados para uma store, você já está pagando juros.
Segundo, esse valor é necessário só dentro de uma tela (como “o painel de filtros está aberto”)? Se sim, não deve ser global.
Terceiro, dá para salvar um ID em vez de duplicar um objeto? Guarde selectedUserId e leia o usuário do cache ou da lista.
Quarto, é derivado? Se pode ser calculado a partir do estado existente, não o armazene.
Finalmente, faça o teste de rastreamento de um minuto. Se um colega não consegue responder “de onde vem esse valor?” (prop, estado local, cache do servidor, URL, store) em menos de um minuto, conserte a propriedade antes de adicionar mais estado.
Imagine um app admin gerado (por exemplo, um gerado a partir de um prompt em Koder.ai) com três telas: lista de clientes, página de detalhe do cliente e um formulário de edição.
O estado se mantém calmo quando tem lares óbvios:
A lista e as páginas de detalhe leem server state do cache de queries. Ao salvar, você não armazena clientes de novo numa store global. Envia a mutation e deixa o cache atualizar ou ser invalidado.
Para a tela de edição, mantenha o rascunho local. Inicialize a partir do cliente buscado, mas trate como separado assim que o usuário começar a digitar. Assim a view de detalhe pode atualizar em segurança sem sobrescrever mudanças pela metade.
UI otimista é onde times frequentemente duplicam tudo. Na maioria dos casos não é necessário.
Quando o usuário clica em Salvar, atualize apenas o registro do cliente em cache e o item correspondente na lista, depois faça rollback se a requisição falhar. Mantenha o rascunho no formulário até o save ser bem-sucedido. Se falhar, mostre um erro e mantenha o rascunho para o usuário tentar de novo.
Suponha que você adicione edição em massa e ela também precise de linhas selecionadas. Antes de criar uma nova store, pergunte: esse estado precisa sobreviver à navegação e ao refresh?
Se sim, coloque na URL (IDs selecionados, filtros). Se não, mantenha no componente da página. Se múltiplos componentes distantes realmente precisam ao mesmo tempo (toolbar + tabela + footer), introduza uma store pequena compartilhada só para esse client state.
Telas geradas podem multiplicar rápido, e isso é ótimo até cada nova tela trazer suas próprias decisões de estado.
Escreva uma nota curta no repositório: o que conta como server state, o que conta como client state e qual ferramenta manda em cada caso. Mantenha curto para que as pessoas realmente leiam.
Adote um hábito no PR: rotule cada novo pedaço de estado como server ou client. Se for server, pergunte “onde carrega, como é cacheado e o que o invalida?” Se for client, pergunte “quem é o dono e quando reseta?”
Se estiver usando Koder.ai (Koder.ai), o Planning Mode pode ajudar a concordar sobre limites antes de gerar novas telas. Um snapshot e rollback dão um jeito seguro de experimentar quando uma mudança de estado sair do esperado.
Escolha uma feature (como editar perfil), aplique as regras de ponta a ponta e deixe isso ser o exemplo que todo mundo copia.
Comece rotulando cada pedaço de estado como server, client (UI) ou derived.
isValid).Depois de rotular, garanta que cada item tenha um dono óbvio (cache de queries, estado local do componente, URL ou uma pequena store).
Use este teste rápido: “Consigo atualizar a página e reconstruir isso a partir do servidor?”
Exemplo: uma lista de projetos é server state; o ID da linha selecionada é client state.
Porque cria duas fontes da verdade.
Se você busca users e copia para useState ou uma store global, agora precisa mantê-los sincronizados durante:
Regra padrão: e crie estado local apenas para preocupações da UI ou rascunhos.
Armazene valores derivados apenas quando você realmente não puder calculá-los de forma barata.
Normalmente calcule a partir de entradas existentes:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingSe o desempenho for um problema real (medido), prefira ou estruturas de dados melhores antes de introduzir mais estado armazenado que pode ficar desatualizado.
Padrão: use uma ferramenta de server-state (comum é TanStack Query) para que componentes simplesmente “peçam” dados e lidem com loading/erros.
Princípios práticos:
Mantenha local por padrão até conseguir nomear um compartilhamento real.
Regra de promoção:
Isso evita que sua store global vire um depósito de flags aleatórias da UI.
Armazene IDs e flags pequenas, não objetos do servidor.
Exemplo:
selectedUserIdselectedUser (objeto copiado)Depois, renderize detalhes buscando o usuário no cache da lista/detalhe. Assim refetches em segundo plano e atualizações funcionam corretamente sem código de sincronização extra.
Trate o formulário como um rascunho (client state) até o envio.
Padrão prático:
Isso evita editar o objeto do servidor “in-place” e conflitos com refetches.
Sinais de alerta:
needsRefresh, didInit, isSaving).Generated screens podem misturar padrões rápido. Uma salvaguarda simples é padronizar propriedade:
Se você usa Koder.ai, use o Planning Mode para decidir limites antes de gerar telas e confie em snapshots/rollback ao experimentar mudanças de estado.
useMemoEvite espalhar refetch() em vários lugares “só para garantir”.
A correção normalmente não é uma nova biblioteca—é deletar espelhos e escolher um dono por valor.