Gerenciamento de estado no React: Context API, Zustand ou Redux?

Entenda quando o useState do próprio componente já basta, quando a Context API resolve, e em que momento vale trocar por uma lib como Zustand ou Redux. Com exemplo prático de cada abordagem.


Já falei aqui sobre custom hook e sobre o reducer do React, mas ficou faltando encarar uma pergunta que aparece cedo em qualquer projeto que cresce: onde o estado do sistema deveria morar? A resposta muda dependendo do tamanho do projeto, e usar a ferramenta errada pro tamanho errado é uma fonte comum de dor de cabeça, tanto usar pouco quanto usar demais.

Primeiro degrau: estado local basta

Antes de pensar em qualquer biblioteca, vale lembrar que a maior parte do estado de uma aplicação não precisa ser compartilhada com ninguém. Se um campo de formulário, um modal aberto ou fechado, um valor de input, só importa pro componente que o contém, useState já resolve, sem complicar nada:

function Formulario() {
  const [nome, setNome] = useState("");
  // esse estado não interessa pra mais ninguém na aplicação
}

O erro mais comum em projeto pequeno não é usar pouca ferramenta, é subir estado local pra um gerenciador global cedo demais, achando que vai precisar compartilhar antes de precisar de fato.

Segundo degrau: Context API

O problema aparece quando um dado precisa ser acessado por componentes distantes na árvore, sem passar prop manualmente por cada nível no meio do caminho (o chamado prop drilling). Tema visual (claro ou escuro) e usuário logado são exemplos clássicos: usados em vários pontos da aplicação, sem relação direta de proximidade entre eles.

A Context API, nativa do React, resolve isso sem precisar de biblioteca externa:

const TemaContext = createContext();
 
function TemaProvider({ children }) {
  const [tema, setTema] = useState("claro");
 
  return (
    <TemaContext.Provider value={{ tema, setTema }}>
      {children}
    </TemaContext.Provider>
  );
}
 
function BotaoTema() {
  const { tema, setTema } = useContext(TemaContext);
 
  return (
    <button onClick={() => setTema(tema === "claro" ? "escuro" : "claro")}>
      Tema atual: {tema}
    </button>
  );
}

Qualquer componente dentro de TemaProvider, não importa a profundidade, acessa tema direto com useContext, sem precisar receber isso via prop.

Onde a Context API começa a doer

O problema da Context API aparece em escala: toda vez que o valor do contexto muda, todo componente que usa useContext daquele contexto re-renderiza, mesmo que só precise de uma parte específica do valor. Num app pequeno isso não incomoda, mas num app grande, com contexto guardando bastante estado e sendo usado em muitos lugares, isso vira re-renderização desnecessária em cascata, difícil de rastrear.

Terceiro degrau: Zustand

Zustand resolve exatamente esse ponto fraco. Em vez de um Provider entregando o valor inteiro pra quem consome, cada componente escolhe exatamente qual pedaço do estado quer observar, e só re-renderiza quando aquele pedaço específico muda:

import { create } from "zustand";
 
const useCarrinhoStore = create((set) => ({
  itens: [],
  adicionar: (item) =>
    set((estado) => ({ itens: [...estado.itens, item] })),
  remover: (id) =>
    set((estado) => ({
      itens: estado.itens.filter((item) => item.id !== id),
    })),
}));
 
function ContadorCarrinho() {
  // só re-renderiza quando "itens" muda, nada mais
  const itens = useCarrinhoStore((estado) => estado.itens);
  return <span>{itens.length} itens</span>;
}

Não tem Provider envolvendo a aplicação, não tem boilerplate de reducer com action type. É a opção que mais gente escolhe hoje quando a Context API já não é suficiente, mas Redux parece peso demais pro tamanho do projeto.

Quarto degrau: Redux

Redux resolve o mesmo problema que Zustand, mas com mais estrutura imposta: toda mudança de estado passa obrigatoriamente por uma action e um reducer centralizado, de forma bem parecida com o useReducer nativo que já expliquei aqui, só que compartilhado pela aplicação inteira.

const carrinhoSlice = createSlice({
  name: "carrinho",
  initialState: { itens: [] },
  reducers: {
    adicionar: (estado, action) => {
      estado.itens.push(action.payload);
    },
    remover: (estado, action) => {
      estado.itens = estado.itens.filter((i) => i.id !== action.payload);
    },
  },
});

Essa estrutura mais rígida vira uma vantagem em projetos grandes, com times grandes: toda mudança de estado é rastreável, previsível e testável isoladamente, e ferramentas como Redux DevTools mostram o histórico exato de cada mudança que aconteceu. O custo é mais boilerplate e uma curva de aprendizado maior, que só se paga em projetos com complexidade real de estado.

Como escolher

Não existe ferramenta "melhor" fora de contexto, existe a ferramenta proporcional ao tamanho do problema. Estado que só importa a um componente fica no useState dele. Estado global simples, tipo tema ou idioma, cabe bem na Context API nativa. Estado global que muda com frequência e afeta partes específicas e diferentes da tela pede Zustand. Projeto grande, com time grande, que precisa de rastreabilidade rígida de cada mudança, é onde Redux realmente compensa o esforço extra.

Fechando

O sintoma de estar usando a ferramenta errada geralmente aparece antes de qualquer decisão consciente: re-renderização estranha demais é sinal de Context sobrecarregado, e um projeto pequeno cheio de action, reducer e store pra guardar três valores é sinal de Redux usado onde useState bastava. Vale revisar de tempos em tempos se o estado do projeto ainda está no degrau certo pro tamanho que ele tem hoje, não pro tamanho que ele tinha quando a decisão foi tomada.