Abra quase qualquer base de código React e você notará o mesmo reflexo. Um desenvolvedor precisa rastrear um valor, então recorre ao useState. Precisa de um contador? useState. Um valor de input temporário? useState. Um booleano para alternar um modal? useState. Em pouco tempo, um único componente detém uma dúzia de hooks separados, cada um gerenciando um fragmento de dados que pode ou não precisar persistir entre as renderizações. O resultado é um código ruidoso, re-renders extras e um estado espalhado como moedas soltas por todo o componente.

O hábito é compreensível. O useState é o primeiro hook que a maioria de nós aprende, e ele funciona. Mas funcionar não é o mesmo que se ajustar. Tratar cada pedaço de dado como estado reativo cria problemas que só aparecem quando o componente cresce.

Só Porque Muda Não Significa que Precise de Estado

Nem toda variável que muda ao longo do tempo pertence ao useState. Alguns valores são meramente o resultado de algo que você já possui. Se você armazenar o nome completo de um usuário no estado apenas porque concatena firstName e lastName, agora você tem duas fontes de verdade. Quando o firstName é atualizado porque o pai sofre um re-render, seu estado fullName fica desatualizado até que você execute outro efeito para sincronizá-lo. Você não precisa de um efeito de sincronização. Você precisa de um valor derivado.

const fullName = `${firstName} ${lastName}`;

Calcule-o durante a renderização. Se a derivação for custosa, memoize-a. Mas não dê a ela seu próprio hook useState, a menos que o usuário possa editar esse nome completo independentemente das partes.

A mesma regra se aplica a listas filtradas. Se você mantiver tanto allItems quanto filteredItems no estado, terá dobrado sua superfície de manutenção. Filtre durante a renderização. Mantenha o array de origem e o texto do filtro no estado, e então derive a lista visível. Isso garante que a lista filtrada nunca esteja fora de sincronia com a origem.

Alguns Valores Nunca Devem Disparar Re-renders

O useState existe especificamente para dizer ao React que algo mudou e que o DOM pode precisar de atualização. Se um valor muda, mas nenhuma parte da UI se importa com essa mudança, o useRef é a ferramenta melhor.

Timers e intervalos são o exemplo clássico. Armazenar IDs de setInterval no estado causa um re-render toda vez que você inicia ou para um timer, mesmo que o usuário não consiga ver o ID do intervalo. Um ref mantém esse valor sem notificar o React. A mesma lógica se aplica ao rastreamento de props anteriores, medição de nós do DOM antes da pintura ou ao armazenamento do último callback para um hook customizado. Pergunte a si mesmo: este valor precisa aparecer na tela? Se a resposta for não, provavelmente ele não precisa de useState.

Os próprios nós do DOM também pertencem aos refs. Embora você possa armazenar um elemento do DOM no estado, fazer isso dispara um re-render após a execução do callback do ref. Na maioria dos casos, você só precisa do nó para um método imperativo ou uma medição, não para renderizá-lo de forma diferente.

A Armadilha dos Booleanos

Preocupações de UI relacionadas tendem a se espalhar quando cada flag recebe seu próprio hook. Você vê componentes com isLoading, isError e isSuccess definidos como três booleanos separados. O problema é que esses três estados não são independentes. Se isLoading e isSuccess forem ambos verdadeiros, sua UI estará em uma condição impossível, mas o TypeScript e o React permitirão que você a renderize de qualquer maneira.

Agrupar estados relacionados evita essas combinações inválidas. Em vez de três booleanos, rastreie uma única string de status: 'idle', 'loading', 'success' ou 'error'. Apenas um pode estar ativo por vez, o que elimina estados impossíveis no nível de tipo. Se os dados forem mais complexos, um objeto com uma união discriminada (discriminated union) limpa as coisas ainda mais. Quando você se pegar atualizando várias chamadas de useState dentro do mesmo manipulador de evento, esse é um sinal de que esses valores pertencem juntos.

Recorra ao useReducer, Não a Outro useState

Existe um ponto em que as atualizações de estado se tornam um jogo de "whack-a-mole". Você chama setA, depois setB, e então condicionalmente setC, tudo dentro de uma única função. O próximo desenvolvedor a ler esse código terá que rastrear toda a sequência para entender o que o componente realmente faz.

O useReducer brilha aqui. Ele não substitui o useState por ser mais avançado; ele substitui o useState porque a lógica exige isso. Um reducer centraliza como o estado muda. Em vez de espalhar imperativos pelos manipuladores de eventos, você despacha uma intenção: dispatch({ type: 'submitted' }). O reducer decide como será o próximo estado. Isso torna os testes triviais, porque sua lógica de estado é uma função pura. Também torna a depuração mais fácil, pois cada mudança deixa uma ação rastreável.

Você não precisa do Redux para justificar um reducer. Se você tiver três ou mais variáveis de estado que atualizam juntas, ou se o seu próximo estado depender fortemente do anterior, um reducer simplifica o componente drasticamente.

Onde o Estado Realmente Reside

Às vezes, o problema não é como você armazena o estado, mas onde. Um erro comum é elevar o estado para um componente pai simplesmente porque ele pode ser necessário em outro lugar. Se apenas um componente folha utiliza uma parte do estado, mantenha-o lá. Isso é colocação (colocation), e reduz o raio de impacto das mudanças. Não faça o pai renderizar novamente porque um filho abriu