O gerenciamento de estado está no centro de todas as aplicações React não triviais. Faça certo e sua base de código será dimensionada perfeitamente; se errar, você acabará tendo pesadelos de perfuração de suporte, interface de usuário obsoleta e componentes sendo renderizados novamente no esquecimento. A boa notícia é que o ecossistema React amadureceu dramaticamente. Agora você tem um rico menu de ferramentas específicas - Redux Toolkit, Zustand, Jotai, TanStack Query e o Context API integrado - cada uma otimizada para uma classe específica de problema.
Este artigo analisa cada solução detalhadamente, explica as vantagens e desvantagens com exemplos concretos e fornece uma estrutura de decisão que você pode aplicar imediatamente aos seus próprios projetos.
Compreendendo os diferentes tipos de Estado
Antes de recorrer a qualquer biblioteca, vale a pena ser preciso sobre que tipo de estado você está realmente gerenciando. A combinação de diferentes categorias de estado é a causa raiz da maioria dos aplicativos React com engenharia excessiva.
- Estado da IU local — se um menu suspenso está aberto, qual guia está ativa, o valor atual de uma entrada controlada. Este estado pertence a um único componente ou a uma pequena subárvore.
- Estado do cliente compartilhado — dados que vários componentes potencialmente distantes precisam ler ou escrever: o usuário atualmente autenticado, preferências de tema, um carrinho de compras.
- Estado do servidor — dados originados em um servidor, são assíncronos por natureza e precisam de invalidação de cache, nova busca em segundo plano e gerenciamento do ciclo de vida de carregamento/erro.
- Estado do URL — filtros, paginação e termos de pesquisa que devem sobreviver à atualização da página e ser compartilháveis por meio de um link.
- Estado do formulário — entradas controladas, erros de validação e status de envio, geralmente melhor tratados por uma biblioteca dedicada, como o React Hook Form.
A coisa mais impactante que você pode fazer é resistir ao impulso de colocar todas as categorias de estado em uma loja global. Cada categoria tem diferentes requisitos de consistência, diferentes tempos de vida e diferentes frequências de atualização.
Contexto React: a opção integrada
O Context API é fornecido com o React e não requer dependências adicionais. Funciona bem para estados que mudam com pouca frequência e são consumidos por muitos componentes — exemplos clássicos são tema, localidade e objeto de usuário autenticado.
// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthState {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
return ctx;
}
A armadilha do desempenho do contexto
O contexto tem uma característica de desempenho bem conhecida que confunde muitos desenvolvedores: todo consumidor renderiza novamente sempre que a referência do valor do contexto muda. Se você armazenar um objeto grande e frequentemente modificado em um único contexto, você acionará novas renderizações desnecessárias em toda a árvore.
As estratégias de mitigação são:
- Divida os contextos por frequência de atualização. Mantenha o objeto de usuário em um contexto e as preferências da UI em outro.
- Memoize o objeto de valor com
useMemoportanto, a referência só muda quando os dados realmente mudam. - Usar
React.memonos componentes do consumidor para pular as novas renderizações quando os adereços de seu interesse não tiverem sido alterados.
A conclusão honesta é que o Contexto é excelente para o estado global de baixa frequência, mas não é uma solução de gerenciamento de estado de uso geral. Depois que você estiver escrevendo memoizações elaboradas para contornar o comportamento de re-renderização do Context, é hora de procurar uma biblioteca dedicada.
Kit de ferramentas Redux: a escolha empresarial madura
Redux tem uma reputação de clichê – uma reputação que conquistou na era pré-Toolkit. O Redux Toolkit (RTK) elimina a cerimônia enquanto preserva o que torna o Redux poderoso: uma árvore de estado única, previsível e inspecionável com fluxo de dados unidirecional estrito.
Navios RTK createSlice, que gera criadores e redutores de ação a partir de uma única definição e usa Immer nos bastidores para que você possa escrever mutações que parecem imperativas, mas na verdade são aplicadas de forma imutável.
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface CartState {
items: CartItem[];
coupon: string | null;
}
const initialState: CartState = { items: [], coupon: null };
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
applyCoupon(state, action: PayloadAction<string>) {
state.coupon = action.payload;
},
},
});
export const { addItem, removeItem, applyCoupon } = cartSlice.actions;
export default cartSlice.reducer;
Consulta RTK: Estado do servidor dentro do Redux
O RTK vem com um complemento chamado RTK Query que lida com o estado do servidor diretamente no armazenamento Redux. Ele gera automaticamente ganchos React a partir de definições de endpoint e gerencia cache, invalidação e atualizações otimistas.
// services/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], void>({
query: () => '/products',
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product>>({
query: ({ id, ...patch }) => ({
url: `/products/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
Redux Toolkit é a escolha certa quando você precisa de depuração de viagem no tempo via Redux DevTools, quando várias equipes precisam contribuir para um modelo de estado compartilhado com convenções aplicadas ou quando você tem lógica de negócios complexa entre fatias que se beneficia de middleware, como redux-saga ou redux-observable.
Zustand: Estado Global Leve Sem Cerimônia
Zustand adota uma abordagem radicalmente diferente. Não há provedor, redutor e nenhum tipo de ação. Você define uma loja como um objeto JavaScript simples com estado e métodos e, em seguida, consome-a com um único gancho. Todo o API cabe em uma tela.
// stores/useCartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
totalPrice: () => number;
clear: () => void;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
};
}
return { items: [...state.items, item] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
clear: () => set({ items: [] }),
}),
{ name: 'cart-storage' }
)
)
);
Assinaturas granulares e desempenho
Zustand resolve o problema de re-renderização do Context por meio de assinaturas baseadas em seletores. Um componente só é renderizado novamente quando a fatia de estado selecionada é alterada.
// Only re-renders when items.length changes, not on price changes
const itemCount = useCartStore((state) => state.items.length);
// Only re-renders when the total changes
const total = useCartStore((state) => state.totalPrice());
O ecossistema de middleware do Zustand cobre persistência até localStorage, integração com DevTools, mutações no estilo Immer e sincronização de URL. É a escolha certa para equipes que desejam capacidade de nível Redux sem custo de configuração de nível Redux.
Jotai: estado atômico inspirado no recuo
Os modelos Jotai são representados como um gráfico de átomos – unidades de estado minúsculas e composíveis que podem ser derivadas umas das outras. Este modelo é particularmente poderoso para estado dinâmico: pense em uma lista de nós editores onde cada nó tem seu próprio estado de seleção/foco independente ou em um formulário complexo onde a visibilidade do campo depende de outros valores de campo.
// atoms/filterAtoms.ts
import { atom, selector } from 'jotai';
export const searchTermAtom = atom('');
export const categoryAtom = atom<string | null>(null);
export const productsAtom = atom<Product[]>([]);
// Derived atom — recomputes only when its dependencies change
export const filteredProductsAtom = atom((get) => {
const products = get(productsAtom);
const term = get(searchTermAtom).toLowerCase();
const category = get(categoryAtom);
return products.filter((p) => {
const matchesTerm = p.name.toLowerCase().includes(term);
const matchesCategory = category === null || p.category === category;
return matchesTerm && matchesCategory;
});
});
// ProductList.tsx
import { useAtom, useAtomValue } from 'jotai';
function ProductList() {
const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
const filtered = useAtomValue(filteredProductsAtom);
return (
<>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
{filtered.map((p) => <ProductCard key={p.id} product={p} />)}
</>
);
}
O modelo de reatividade refinado de Jotai significa que quando o termo de pesquisa muda, apenas os componentes que realmente assinam filteredProductsAtom renderizar novamente. Componentes assinados apenas para categoryAtom estão intocados. Isso torna o Jotai excelente para aplicações com gráficos de estado densos e interdependentes.
Consulta TanStack: o local certo para o estado do servidor
TanStack Query (anteriormente React Query) não é uma biblioteca geral de gerenciamento de estado — é uma biblioteca de estado de servidor e lida com essa responsabilidade melhor do que qualquer outra coisa no ecossistema. Buscar, armazenar em cache, sincronizar e atualizar dados remotos no React sem TanStack Query normalmente significa gerenciar manualmente sinalizadores de carregamento, objetos de erro e invalidação de cache no estado local ou em um armazenamento global. O TanStack Query automatiza tudo isso.
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => api.getProducts(filters),
staleTime: 5 * 60 * 1000, // treat data as fresh for 5 minutes
gcTime: 10 * 60 * 1000, // keep unused data in cache for 10 minutes
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (update: ProductUpdate) => api.updateProduct(update),
// Optimistic update
onMutate: async (update) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previous = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((p) => (p.id === update.id ? { ...p, ...update } : p))
);
return { previous };
},
onError: (_err, _update, context) => {
queryClient.setQueryData(['products'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
A matriz de chave de consulta é a primitiva de cache do TanStack Query. Qualquer consulta que compartilhe a mesma chave compartilha a mesma entrada de cache. Quando você invalida uma chave, cada componente inscrito nessa chave é automaticamente buscado novamente em segundo plano e atualizado quando novos dados chegam.
Pré-busca e hidratação em Next.js
Em um aplicativo Next.js, você pode pré-buscar consultas no servidor e desidratá-las na carga HTML, eliminando totalmente o carregamento de spinners para a renderização inicial da página.
// app/products/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProductsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['products', {}],
queryFn: () => api.getProducts({}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductList />
</HydrationBoundary>
);
}
Combinando Bibliotecas: Uma Arquitetura Prática
As aplicações reais quase sempre se beneficiam da combinação de múltiplas ferramentas, cada uma responsável por sua própria categoria de estado. Uma aplicação React de médio a grande porte bem estruturada pode ter esta aparência:
- Consulta TanStack possui todo o estado do servidor. Ele busca, armazena em cache e sincroniza dados remotos. Nada mais afeta as respostas do API.
- Zustand possui o estado global do cliente – o carrinho de compras, a sessão do usuário autenticado, as preferências da UI e qualquer coordenação entre recursos que seja muito complexa para o Context.
- Contexto React possui configuração de baixa rotatividade - o tema atual, a localidade, sinalizadores de recursos.
- useState/useReducer próprio estado do componente local — modal aberto/fechado, valores de campo de formulário (ou formulário de gancho React para formulários complexos), seleção de guias.
- Parâmetros de pesquisa de URL próprio estado de navegação — filtros ativos, ordem de classificação, número da página atual.
Esta separação de preocupações não é apenas estética. Isso significa que o cache do estado do servidor nunca fica poluído com o estado da UI, o armazenamento global do cliente nunca aumenta com dados de resposta que o TanStack Query armazenaria em cache com mais eficiência e o seu contexto nunca aciona novas renderizações em todo o aplicativo porque alguém armazenou um valor que muda rapidamente nele.
Padrões de desempenho que vale a pena conhecer
Seletores memorizados com nova seleção
Ao derivar valores computados de um armazenamento Redux, use Reselect para evitar recomputação em cada renderização. Reexportações RTK createSelector de Selecionar novamente.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state: RootState) => state.cart.items;
export const selectCartSummary = createSelector(selectItems, (items) => ({
count: items.reduce((n, i) => n + i.quantity, 0),
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
Dividindo renderizações com useTransition
React 18 useTransition permite marcar uma atualização de estado como não urgente para que o navegador possa interrompê-la para lidar com trabalhos de maior prioridade, como entradas do usuário. Isso é particularmente útil quando uma mudança de estado aciona uma nova renderização cara.
const [isPending, startTransition] = useTransition();
const handleFilterChange = (value: string) => {
startTransition(() => {
setFilterValue(value); // expensive downstream re-render is deferrable
});
};
Evitando problemas de identidade de objeto
Uma fonte comum de re-renderizações fantasmas é a criação de novos objetos ou literais de array dentro da renderização. useMemo e useCallback são suas ferramentas aqui, mas aplique-as somente quando tiver evidências de um problema de desempenho – a memorização prematura adiciona sobrecarga cognitiva sem benefício garantido.
Quadro de Decisão
Use a seguinte árvore de decisão ao escolher uma abordagem de gerenciamento de estado para um novo recurso:
- O estado é puramente local para um componente ou para uma pequena subárvore? Usar
useStateouuseReducer. - O estado representa dados obtidos de um servidor? Usar Consulta TanStack.
- É o estado do cliente global que muitos componentes da árvore precisam? Alcançar Zustand a menos que você já esteja em uma base de código Redux.
- O estado muda raramente e serve a um propósito de configuração (tema, localidade)? Usar Contexto React.
- O estado está altamente interconectado com muitos valores derivados? Considere Jotai.
- Você precisa de depuração de viagem no tempo, convenções arquitetônicas rígidas ou sagas assíncronas complexas? Usar Kit de ferramentas Redux.
Antipadrões comuns a serem evitados
- Armazenando respostas do servidor em Redux ou Zustand. Deixe o TanStack Query possuir esses dados. Duplicá-lo em um armazenamento global cria bugs de sincronização.
- Colocando tudo em uma única loja enorme. O estado não relacionado pertencente à mesma fatia força o acoplamento desnecessário e torna os testes mais difíceis.
- Usando Contexto para atualizações de alta frequência. Cada mudança de valor de contexto renderiza novamente todos os consumidores. Para valores que mudam a cada pressionamento de tecla ou quadro de animação, Context é a ferramenta errada.
- Derivando o estado dentro dos componentes. Os valores calculados que dependem do estado devem ser seletores memorizados ou átomos derivados, e não cálculos embutidos executados em cada renderização.
- Ignorando estados de carregamento e erro. O estado do servidor é inerentemente assíncrono. Superfícies de consulta TanStack
isLoading,isErroreerrorpara cada consulta; use-os.
Conclusão
O cenário de gerenciamento de estado na React nunca foi tão saudável. Você não precisa mais escolher entre uma estrutura pesada e construir tudo do zero. Redux Toolkit oferece previsibilidade de nível empresarial sem o padrão de seu antecessor. Zustand traz o mesmo poder com uma fração da configuração. Jotai resolve problemas difíceis em gráficos de estado dinâmicos e refinados. E o TanStack Query resolveu definitivamente o estado do servidor — se você ainda estiver gerenciando respostas API em useEffect e useState, você deve migrar a si mesmo.
O insight mais importante é categórico: identifique primeiro com que tipo de estado você está lidando e, em seguida, selecione a ferramenta otimizada para essa categoria. Resista ao instinto de canalizar tudo em um único sistema. Uma abordagem em camadas – TanStack Query para estado do servidor, Zustand para estado global do cliente, Context para configuração e estado local para todo o resto – produz bases de código que são mais fáceis de raciocinar, mais fáceis de testar e muito mais fáceis de manter à medida que as equipes e os requisitos crescem.