Gerenciamento de estados: React Context vs Zustand vs Jotai
Você já passou uma tarde inteira debugando uma atualização que não disparava reação no componente, só para descobrir que o problema estava no useMemo que envolveu seu contexto? Ou já viu um projeto crescer até o ponto em que qualquer mudança num estado global acaba afetando metade da árvore de componentes? Gerenciamento de estado em React não é só sobre guardar dados — é sobre prevenir esses choques de realidade que te fazem questionar sua carreira.
O React é poderoso, mas ele não vem com uma solução pronta para o que acontece quando o useState começa a vazar pelo app inteiro. Com o tempo, você percebe que state management não é um nice to have — é o que separa um código sustentável de uma estrutura que desaba com a primeira feature nova.
Tive que lidar com isso de verdade num projeto recente: o inventory-service, que sincroniza catálogo e estoque entre OSPOS e marketplaces. A principio, parecia simples — um estado global para o carrinho, outro para o estoque. Mas quando você começa a lidar com atualizações concorrentes, retry automático de requisições e múltiplos endpoints, o estado começa a ter vida própria. Foi ali que decidi investir tempo em entender as opções além do Context.
Vou comparar três abordagens que encontrei na prática: React Context (a nativa), Zustand (a minimalista) e Jotai (a atômica). Nada de teoria pura — o que funciona, o que dói e em que cenário cada um se comporta bem.
Quando o contexto já não é suficiente
O React Context é nativo, não precisa de dependências externas e funciona. É verdade. Mas funciona bem?
O Context é ótimo para valores que mudam raramente: temas, autenticação, configurações globais. Por exemplo, num projeto de ONG que fiz (Mensageiros da Esperança), o contexto serve bem para armazenar o usuário logado e permissões — mudam só no login/logout.
O problema surge quando você tenta usar o Context para estado que muda com frequência: inputs de formulários, lists de produtos em carrinho, status de carregamento. Cada atualização disso dispara re-renders em todos os consumers, mesmo que o valor consumido por aquele componente específico não tenha mudado. É o famoso "over-rendering".
Num dos meus primeiros projetos com React (um dashboard para ricing Linux), comecei com Context para tudo. O resultado? Quando o usuário mudava o slider de transparência do Hyprland (sim, isso era um estado no app), o app inteiro re-renderizava — e isso incluía listas de pacotes, previews de temas, configurações. Não era sustentável.
O Context é simples de entender, mas sua implementação interna (o provider como componente com estado próprio) cria uma dependência de renderização que muitas vezes é mais pesada do que parece.
Zustand: API simples, resultados reais
Zustand surgiu como resposta a essa complexidade. Sua API é mínima — basicamente, você cria um store com create, passa um estado inicial e mutações. Não precisa de providers, não precisa de hooks complexos, nem memoização manual.
// Exemplo real do inventory-service: gerenciamento de estoque
import { create } from 'zustand';
interface StockState {
items: Record<string, number>; // SKU -> quantidade
loading: boolean;
error: string | null;
fetchStock: () => Promise<void>;
updateQuantity: (sku: string, delta: number) => void;
}
export const useStockStore = create<StockState>((set) => ({
items: {},
loading: false,
error: null,
fetchStock: async () => {
set({ loading: true, error: null });
try {
// Simulação — no real, chama API do Mercado Livre/OSPOS
const response = await fetch('/api/stock');
const data = await response.json();
set({ items: data, loading: false });
} catch (err) {
set({ error: 'Falha ao carregar estoque', loading: false });
}
},
updateQuantity: (sku, delta) => {
set((state) => ({
items: {
...state.items,
[sku]: (state.items[sku] || 0) + delta,
},
}));
},
}));
No inventory-service, usei Zustand porque precisava de state global que fosse fácil de testar e persistir. O Zustand tem suporte nativo a middleware, então adicionei um persist que salva o estoque em localStorage — útil quando o sync com o mercado falha e você quer manter o estado local funcional.
Uma coisa que me chamou atenção: o Zustand evita o "wrapper hell" do Context. Você usa o hook direto no componente, sem precisar aninhar providers ou se preocupar com renderizações desnecessárias. O seletor do useStore permite pegar só o que o componente precisa:
function StockBadge({ sku }: { sku: string }) {
const quantity = useStockStore(
(state) => state.items[sku] ?? 0,
shallow
);
return (
<span className={quantity < 10 ? 'text-red-500' : ''}>
{quantity} un.
</span>
);
}
O shallow (do zustand/shallow) compara referências superficialmente — importante quando você retorna objetos ou arrays no seletor. Isso evita re-renders mesmo que o restante do estado mude.
Jotai: atomic state, sem complicações
Jotai segue outro princípio: estado atômico. Em vez de um único objeto grande, você divide o estado em pequenas unidades ("atoms") que podem ser combinadas livremente.
// Exemplo do projeto Plexo: gerenciamento de tasks
import { atom } from 'jotai';
export const tasksAtom = atom<Task[]>([]);
export const filteredTasksAtom = atom((get) => {
const tasks = get(tasksAtom);
const filter = get(filterAtom);
return tasks.filter(task => task.status === filter);
});
export const activeTasksCountAtom = atom((get) => {
const tasks = get(tasksAtom);
return tasks.filter(t => t.status === 'active').length;
});
Num dos projetos do ecossistema de IA local (o provider-health-daemon), usei Jotai para o dashboard. O painel precisa mostrar health status de múltiplos providers, mas cada provider tem configurações independentes. Com atoms, posso criar providerStatusAtom(providerId), providerConfigAtom(providerId) — sem precisar estruturar um objeto gigante.
A parte mais legal é a composição: filteredTasksAtom é automaticamente recalculado sempre que tasksAtom ou filterAtom mudam. Não precisa de useMemo, useSelector, ou lógica extra. O Jotai já sabe quais dependências cada atom tem e recalcula apenas o necessário.
Para componentes, usa-se o useAtomValue ou useAtom:
function TaskList() {
const tasks = useAtomValue(filteredTasksAtom);
return (
<ul>
{tasks.map(task => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}
E se você precisa mutate? O Jotai tem atoms mutáveis (com write), mas recomenda-se preferir atoms derivados sempre que possível. Isso tira a tentação de colocar lógica imperativa direto no estado.
No Plexo, usei Jotai para o state de UI (tabs abertas, filtros, seleção atual) e mantive a lógica de persistência separada com middleware customizado. A separação de responsabilidades ficou clara: atoms para dados, hooks/services para efeitos colaterais.
Comparando performance
Performance não é só sobre quantos ms leva para atualizar — é sobre quantos componentes re-renderizam, quantas verificações de igualdade são feitas e como o garbage collector se sente.
No contexto tradicional (sem memoização), cada atualização de contexto dispara re-renders em todos os components que consumem aquele contexto, mesmo que o valor específico usado por cada um não mude. O React não tem como saber disso, já que o objeto retornado pelo provider é novo a cada render.
Zustand evita isso com o hook useStore que usa referência de estado interno (via useSyncExternalStore) e comparação seletiva. Só quem usa o estado modificado re-renderiza. No testes que fiz com listas de 100+ itens (como no dashboard do inventory-service), o Zustand manteve 60fps mesmo com atualizações frequentes.
Jotai brilha quando você tem muitos átomos interdependentes. Cada atom sabe quais outros atoms ele lê, então só recalcula os derivados que precisam. Num cenário de filter + search + pagination (como num dos meus tests de prototype), Jotai re-renderizou apenas o necessário: a lista, não os filtros.
Mas cuidado com abuso de atoms derivados em cascata. Se você tiver 10 levels de atom((get) => get(anotherAtom)), cada mudança no fundo dispara recalculo em todos. Jotai é rápido, mas não mágico.
Context sem memoização: lento. Context com useMemo em cada provider e React.memo em cada consumer: trabalhoso, mas possível. Zustand/Jotai: menos código, menos surpresas.
Quando usar cada um
React Context:
- Valores estáticos ou que mudam raramente (auth, tema, configurações globais)
- Projetos pequenos onde a simplicidade de não ter dependências pesa mais que performance
- Quando você já tem uma árvore de componentes complexa e não quer reescrever
Zustand:
- Estado global que muda com frequência (carrinhos, forms, lists)
- Quando você quer uma API simples e direta
- Para projetos onde testabilidade e persistência são importantes
- Quando você precisa de middleware personalizado (persist, logger, middleware de API)
Jotai:
- Estados complexos com muitos valores interdependentes (dashboards, ferramentas de editing)
- Quando você quer evitar state nesting e preferir composição
- Para componentes que precisam de múltiplos valores independentes
- Quando performance de atualização específica é crítica (ex: grids grandes, lists com drag-and-drop)
No lead-pipeline, por exemplo, usei Zustand para o estado global do pipeline (passos, status atual) e Jotai para o editor de lead (formulário com many fields, validações). Os dois se complementam bem.
O que fazer se já usa Context?
Migração não precisa ser de um dia para o outro. Você pode fazer por camadas.
-
Identifique os contextos problemáticos: aqueles que causam re-renders em cascata, ou que você precisou memoizar manualmente várias vezes.
-
Extraia para um store: pegue o valor do contexto e crie um Zustand/Jotai equivalente. Não mude nada no componente ainda.
-
Substitua consume um por um: comece pelos componentes mais simples. Se você usa
useContext(MyContext)e o valor é usado direto, mude parauseMyStore((s) => s.value). -
Use middleware para migração: Zustand tem
persistque lê JSON antigo do localStorage, então você pode migrar valores antigos de Context para Zustand sem perder estado.
No OSPOS (o PDV que customizei na loja), comecei com Context para o carrinho de compras. Quando a loja cresceu e a lista de produtos passou de 500 itens, os re-renders começaram a ser perceptíveis. Migrar para Zustand levou um final de semana: 300 linhas de código, menos linhas de boilerplate (não precisa mais do CartContext.Provider aninhado no App) e resposta mais rápida.
A parte mais fácil de migrar foi a persistência: o Context já salva carrinho em localStorage. No Zustand, só adicionei middleware: persist e ajustei a chave. Pronto.
Conclusão
State management em React não é sobre escolher o mais popular — é sobre escolher o que resolve o seu problema, com o mínimo de complexidade.
-
Context ainda é a opção mais simples para estados estáveis e valores que não mudam com frequência. Mas não se engane: com estado dinâmico, ele pode se tornar um pesadelo de performance.
-
Zustand é a minha escolha padrão hoje: API simples, performance boa, flexível o suficiente para middleware customizado. É o "middle ground" entre minimalismo e poder.
-
Jotai brilha em aplicações onde o estado é naturalmente modular — dashboards, ferramentas de editing, qualquer coisa com muitos valores interdependentes.
No fim das contas, o melhor state management é aquele que você entende e consegue manter. Não vale trocar Zustand por Context só porque é nativo — ou vice-versa. Mas saber quando cada um faz sentido? Isso sim faz a diferença entre um projeto que escala e um que colapsa com a primeira feature nova.
Takeaways práticos
- Comece com
useStatelocal e contextos simples. Migre quando a dor for real, não antes. - Se você precisa de persistência ou middleware, Zustand paga better.
- Para estados complexos com many derivadas, Jotai evita re-renders em cascata.
- Nunca use contexto sem memoização para estado que muda com frequência — ou use
useContextcomuseMemono provider eReact.memonos consumers (mas isso já tira a simplicidade). - Teste performance com DevTools Profiler: veja quantos componentes re-renderizam com uma atualização.
O state management é invisível até quando quebra. Investir tempo em escolher certo vale mais que escolher "certo" hoje e ter que reescrever amanhã.
Glossário do post
- react
- Biblioteca JavaScript para construir interfaces de usuário a partir de componentes reutilizáveis, mantida pela Meta.
- service
- Unidade de software com responsabilidade bem definida, exposta por uma interface e reutilizável por outras partes do sistema.
- api
- Application Programming Interface: conjunto de regras que permite a um programa conversar com outro, expondo dados e funcionalidades de forma padronizada.
- middleware
- Função que intercepta requisições em um servidor para executar lógica antes ou depois do handler, como autenticação e logs.
- ui
- User Interface: a camada visual com a qual o usuário interage diretamente em um aplicativo ou site.
- json
- JavaScript Object Notation: formato leve de troca de dados baseado em pares chave/valor, legível para humanos e máquinas.
- pdv
- Ponto de Venda: sistema utilizado para registrar e processar vendas diretamente no balcão de um comércio.