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

O que dá errado com estado em apps React reais
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:
- Não ressincronize dados do servidor em estado cliente sem uma razão clara.
- Não promova estado local da UI para uma store global só porque pode ser útil depois.
- Não permita que valores derivados virem estado próprio a não ser que calcular seja realmente caro.
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.
Server state vs client state em linguagem simples
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?”
- Se sim, provavelmente é server state.
- Se não, provavelmente é client state.
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.
Um conjunto chato de regras que previne a maioria dos problemas
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.
- Trate dados da API como algo a ser buscado e cacheado, não como algo a manter manualmente no estado do componente.
- Não clone dados buscados em estado local “por precaução”. Cópias criam deriva.
- Mantenha estado cliente perto de onde é usado. Suba apenas quando partes separadas da árvore realmente precisarem dele.
- Armazene IDs e flags pequenas, não objetos inteiros. Re-obtenha o objeto do cache quando renderizar.
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.
Como lidar com server state sem complicar demais
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:
- Invalide a chave de query exata que ficou obsoleta.
- Atualize o cache para a query exata que mudou.
- Navegue para uma tela que já busca a query correta.
Padrões de client state que se mantém pequenos
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.
Uma regra simples de promoção
Só mova estado para cima quando você conseguir nomear o problema de compartilhamento.
- Mantenha estado apenas da UI local por padrão.
- Eleve para o pai comum mais próximo quando irmãos precisarem.
- Use uma store compartilhada pequena só quando múltiplas rotas ou componentes distantes precisarem do mesmo valor ao mesmo tempo.
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.
Formatos de store que continuam legíveis
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.
Formulários, rascunhos e estado temporário da UI
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.
Passo a passo: simplificando um setup de estado bagunçado
À 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:
- Faça um inventário do seu estado. Liste o que você tem (listas, item selecionado, filtros, modais, rascunhos) e rotule cada um como server ou client.
- Delete duplicatas. Remova estado como
filteredUsersse puder calcular a partir deusers+filter. PrefiraselectedUserIdao invés de umselectedUserduplicado. - Centralize o fetch em uma camada. Use uma abordagem única para server-state para que cache, refetch e invalidação sigam uma única regra.
- Adicione uma store cliente minúscula só onde realmente vale a pena (necessidades cross-page como rascunho de um assistente).
- Trave regras de nomeclatura para que a bagunça não volte.
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:
- Queries:
users.list,users.detail(id) - Client state:
ui.isCreateModalOpen,filters.userSearch - Actions:
openCreateModal(),setUserSearch(value) - Escritas no servidor:
users.create,users.update,users.delete
O objetivo é uma fonte de verdade por coisa, com limites claros entre server state e client state.
Sinais iniciais de que a complexidade do estado vai explodir
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:
- Você não consegue explicar “de onde esse valor vem” e “quem é o dono” em uma frase.
- Sua store ou reducer importa clientes de API ou conhece códigos de status HTTP.
- Cada nova tela adiciona flags compartilhadas como
needsRefresh,didInit,isSavingque ninguém remove. - Você escreve efeitos principalmente para espelhar um estado em outro.
- Corrigir um bug significa atualizar o mesmo campo em várias camadas.
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.
Armadilhas comuns e como evitá-las
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.
Checklist rápido antes de adicionar mais estado
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.
Exemplo: um app CRUD que continua sem surpresas enquanto cresce
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:
- Server state: lista de clientes, registro do cliente, requisições de salvar/excluir.
- Client state: filtros da lista, linha selecionada, rascunho de edição.
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.
Uma atualização otimista sem exagero
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.
Quando uma segunda feature quer o mesmo estado
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.
Próximos passos: mantenha suas regras consistentes em apps gerados
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.
Perguntas frequentes
Qual a maneira mais simples de desenredar estado bagunçado em React?
Comece rotulando cada pedaço de estado como server, client (UI) ou derived.
- Server: dados buscados como usuários, tarefas, permissões.
- Client: escolhas da interface como “modal aberto”, aba selecionada, texto do filtro.
- Derived: valores que você pode calcular (lista filtrada, totais,
isValid).
Depois de rotular, garanta que cada item tenha um dono óbvio (cache de queries, estado local do componente, URL ou uma pequena store).
Como eu sei se algo é server state ou client state?
Use este teste rápido: “Consigo atualizar a página e reconstruir isso a partir do servidor?”
- Se sim, trate como server state (buscar/cache/refazer fetch).
- Se não, trate como client state (estado local da UI, talvez uma store).
Exemplo: uma lista de projetos é server state; o ID da linha selecionada é client state.
Por que copiar dados da API para estado local ou global é tão problemático?
Porque cria duas fontes da verdade.
Se você busca users e copia para useState ou uma store global, agora precisa mantê-los sincronizados durante:
- refetches em segundo plano
- atualizações vindas de outras abas/usuários
- atualizações parciais após mutações
Regra padrão: leia dados do servidor de um lugar (o cache de queries) e crie estado local apenas para preocupações da UI ou rascunhos.
Quando é aceitável armazenar valores derivados em vez de calculá-los?
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 && !isSaving
Se o desempenho for um problema real (medido), prefira useMemo ou estruturas de dados melhores antes de introduzir mais estado armazenado que pode ficar desatualizado.
Qual a forma “sem surpresas” de lidar com server state em React?
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:
- Use chaves de query estáveis e consistentes (inclua só entradas que mudam o resultado).
- Para escritas, use mutations e escolha uma estratégia por recurso:
- invalidar a query exata de lista/detalhe, ou
- atualizar o cache de forma direcionada.
Evite espalhar refetch() em vários lugares “só para garantir”.
Quando devo mover estado da UI para uma store global?
Mantenha local por padrão até conseguir nomear um compartilhamento real.
Regra de promoção:
- Estado local no componente por padrão.
- Eleve para o pai comum mais próximo quando irmãos precisarem dele.
- Use uma pequena store compartilhada só quando múltiplos componentes/distintas rotas realmente precisarem do mesmo valor ao mesmo tempo.
Isso evita que sua store global vire um depósito de flags aleatórias da UI.
Devo guardar o objeto selecionado ou apenas seu ID?
Armazene IDs e flags pequenas, não objetos do servidor.
Exemplo:
- ✅
selectedUserId - ❌
selectedUser(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.
Qual a melhor forma de gerenciar formulários e rascunhos?
Trate o formulário como um rascunho (client state) até o envio.
Padrão prático:
- Busque o registro salvo como server state.
- Inicialize um rascunho local com esses valores.
- Deixe o usuário editar o rascunho livremente.
- No save, submeta o rascunho e atualize/refresh o cache do servidor.
- No cancel, descarte o rascunho e mostre a versão do servidor.
Isso evita editar o objeto do servidor “in-place” e conflitos com refetches.
Quais os sinais de que a complexidade do estado vai explodir?
Sinais de alerta:
- O mesmo dado existe em um componente, numa store global e no cache de queries.
- Efeitos que espelham estado pra lá e pra cá ("quando a query muda, atualize a store", "quando a store muda, refetch").
- Flags compartilhadas acumulando (
needsRefresh,didInit,isSaving). - Você não consegue explicar “de onde vem esse valor?” em uma frase.
A correção normalmente não é uma nova biblioteca—é deletar espelhos e escolher um dono por valor.
Como manter o estado “sem surpresas” ao usar um gerador como Koder.ai?
Generated screens podem misturar padrões rápido. Uma salvaguarda simples é padronizar propriedade:
- Server state: sempre via a mesma camada de fetch/cache.
- Client UI state: local por padrão, store só quando necessário.
- Derived values: calcule, não armazene.
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.