Estado do React que pertence ao servidor
A maioria dos useState que audito guarda uma cópia do banco. Coloque esse estado onde ele mora e os bugs vão junto.
Abra uma base de código React madura e conte as chamadas de useState. Depois conte quantas delas guardam algo que veio do servidor: um usuário, uma lista de pedidos, o preço de um produto. Na maioria dos projetos que audito, mais da metade do estado de cliente é cópia de uma linha do banco, e cada uma dessas cópias é uma chance de estar errada.
O App Router mudou qual é a resposta certa. Antes dele, o cliente precisava guardar dados do servidor porque não havia outro lugar para renderizar. Agora há, e a pergunta para cada pedaço de estado é simplesmente: quem é o dono disso?
Três tipos de estado
Estado de servidor é dado que o banco possui. Pedidos, usuários, preços, permissões. O servidor é a fonte da verdade e o cliente só guarda um snapshot. Estado de cliente é dado que existe só na sessão do navegador: se um menu está aberto, qual aba está selecionada, o que o usuário digitou mas não enviou. Estado de URL é dado que deve sobreviver a um refresh e ser compartilhável: filtros, paginação, o intervalo de datas selecionado.
Os bugs vêm de colocar um tipo no recipiente do outro. Estado de servidor em useState fica obsoleto no momento em que outra pessoa muda a linha. Estado de cliente no servidor significa uma ida e volta para abrir um dropdown. Filtros em useState significam que o botão de voltar quebra e o link não pode ser compartilhado.
Estado de servidor vive em Server Components
Um Server Component lê os dados de que precisa, no momento da requisição, do banco. Passa os campos que o cliente precisa como props. Não há flag de loading, nem estado de erro, nem effect que busca depois de montar, nem chave de cache para inventar. O snapshot é tão fresco quanto a requisição.
Quando o usuário muda algo, uma Server Action executa a mutação e chama revalidatePath ou revalidateTag. O framework renderiza de novo os Server Components afetados e envia a árvore nova ao cliente por streaming. O componente que mostrava o valor antigo nunca o guardou em estado, então não há nada para sincronizar.
Para o intervalo entre o clique e a resposta, useOptimistic guarda um valor temporário que a resposta do servidor substitui. Essa é a única cópia legítima de estado de servidor no cliente, e ela tem o escopo de uma única action pendente em vez de viver em uma store.
O padrão que mais removo em auditorias é o fetch em useEffect que define estado. Ele cria uma cascata de requisições, um flash de conteúdo vazio, uma corrida quando o componente remonta e uma cópia obsoleta que dura mais do que sua utilidade. Mover esse fetch para o Server Component apaga os quatro problemas e cerca de trinta linhas.
Estado de URL vive na URL
Search params são a casa certa para qualquer coisa que um usuário queira salvar ou compartilhar. Um Server Component os lê como prop e consulta de acordo. Um Client Component os atualiza pelo router, e o servidor renderiza de novo. O estado é visível, sobrevive a reloads e o botão de voltar funciona.
A regra que uso é que, se dois usuários olhando a mesma URL deveriam ver a mesma coisa, o estado que decide isso pertence à URL. Se o estado é privado desta sessão, como uma busca digitada pela metade, fica no cliente.
Quando buscar no cliente ainda é o certo
Nada disso torna bibliotecas de dados no cliente obsoletas. Telas altamente interativas que rebuscam ao ganhar foco, consultam atualizações periodicamente ou precisam funcionar offline ainda se beneficiam de um cache de cliente com ciclo de vida próprio. Um dashboard em tempo real, um editor colaborativo, um app que precisa funcionar no trem sem sinal.
O erro é usar essa maquinaria por padrão, para uma página de produto que muda uma vez por dia, porque era a única opção em uma arquitetura mais antiga. Comece com o servidor sendo dono do estado de servidor. Recorra a um cache de cliente quando a tela tem um requisito que o servidor não atende, e saiba dizer qual é esse requisito.
Estado tem dono. Coloque onde o dono está, e a maior parte do código de sincronização que você vinha escrevendo se revela uma gambiarra para tê-lo colocado em outro lugar.