TL;DR: A combinação de Zustand para estado do cliente e TanStack Query para estado do servidor cria uma solução elegante e performática para gerenciamento de estado em React, reduzindo boilerplate e melhorando a experiência do desenvolvedor.
Por Que Esta Combinação Funciona Tão Bem
Antes de mergulhar nos detalhes técnicos, vamos entender por que Zustand e TanStack Query se complementam perfeitamente:
Zustand lida com estado do cliente — preferências do usuário, estado da UI, dados de formulários e lógica específica da aplicação que vive inteiramente no seu app.
TanStack Query lida com estado do servidor — dados buscados de APIs, cache, sincronização e toda a complexidade de gerenciar dados que vivem no seu servidor.
Essa clara separação de preocupações elimina a confusão que costumava ter sobre onde diferentes tipos de estado deveriam viver.
Zustand: Redux Sem a Cerimônia
Zustand (alemão para "estado") é uma solução de gerenciamento de estado pequena, rápida e escalável. O que a torna especial é como parece com Redux, mas sem todo o boilerplate.
Configuração Básica do Zustand
Veja o quão simples é criar um store:
import { create } from 'zustand';
const useCounterStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// Valores computados
isEven: () => get().count % 2 === 0,
}));
// Uso em componentes
function Counter() {
const { count, increment, decrement, reset, isEven } = useCounterStore();
return (
<div>
<p>Contagem: {count} ({isEven() ? 'par' : 'ímpar'})</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Resetar</button>
</div>
);
}O que eu amo nisso:
Nenhum provider necessário
Sem action creators ou reducers
Sem connect() ou mapStateToProps
Acesso direto ao estado e ações
Suporte a TypeScript pronto para usar
Padrões Avançados do Zustand
À medida que sua aplicação cresce, o Zustand escala de forma elegante. Aqui estão os padrões que uso em produção:
Dividindo Grandes Stores
Em vez de uma store massiva, crie fatias focadas:
// Fatia de usuário
const createUserSlice = (set, get) => ({
user: null,
isAuthenticated: false,
login: async (credentials) => {
set({ isLoading: true });
try {
const user = await authAPI.login(credentials);
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({ error: error.message, isLoading: false });
}
},
logout: () => {
set({ user: null, isAuthenticated: false });
},
});
// Fatia de UI
const createUISlice = (set, get) => ({
theme: 'light',
sidebar: { isOpen: false },
notifications: [],
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
toggleSidebar: () => set((state) => ({
sidebar: { isOpen: !state.sidebar.isOpen }
})),
addNotification: (notification) => set((state) => ({
notifications: [...state.notifications, {
id: Date.now(),
...notification
}]
})),
removeNotification: (id) => set((state) => ({
notifications: state.notifications.filter(n => n.id !== id)
})),
});
// Fatia de carrinho de compras
const createCartSlice = (set, get) => ({
items: [],
isOpen: false,
addItem: (product) => set((state) => {
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
items: [...state.items, { ...product, quantity: 1 }]
};
}),
// ... outras funções do carrinho
});
// Combinar fatias
const useAppStore = create((...args) => ({
...createUserSlice(...args),
...createUISlice(...args),
...createCartSlice(...args),
}));Middleware e Persistência
O sistema de middleware do Zustand é incrivelmente poderoso. Veja como adiciono persistência e depuração:
import { persist, createJSONStorage } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
const useAppStore = create(
devtools(
persist(
(...args) => ({
...createUserSlice(...args),
...createUISlice(...args),
...createCartSlice(...args),
}),
{
name: 'app-storage',
storage: createJSONStorage(() => localStorage),
// Persistir apenas partes específicas do store
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
theme: state.theme,
cart: {
items: state.items
}
}),
}
),
{
name: 'app-store',
}
)
);Esta configuração oferece:
Persistência automática no localStorage
Integração com Redux DevTools
Persistência seletiva (não salvar estado temporário da UI)
Depuração com viagem no tempo
Subscriptions e Atualizações Externas
Às vezes você precisa reagir às mudanças do store fora de componentes React:
const useAppStore = create((set, get) => ({
// ... definição do store
}));
// Inscrever-se em mudanças específicas
const unsubscribe = useAppStore.subscribe(
(state) => state.user,
(user, previousUser) => {
if (user && !previousUser) {
// Usuário acabou de fazer login
analytics.track('user_login', { userId: user.id });
} else if (!user && previousUser) {
// Usuário acabou de fazer logout
analytics.track('user_logout');
}
}
);TypeScript com Zustand
O suporte a TypeScript do Zustand é excelente. Veja como estruturo stores tipados:
interface UserState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
interface UserActions {
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
clearError: () => void;
}
// ... outras interfaces
type AppStore = UserState & UserActions & UIState & UIActions;
const useAppStore = create<AppStore>((set, get) => ({
// Implementação do store
}));TanStack Query: Estado do Servidor Feito Direito
Enquanto o Zustand lida elegantemente com o estado do cliente, o TanStack Query (antigo React Query) revoluciona como você trabalha com estado do servidor. Ele lida com cache, sincronização, atualizações em segundo plano e toda a complexidade de gerenciar dados que vêm da sua API.
O Modelo Mental
Antes do TanStack Query, eu tratava dados de servidor como estado regular:
// A forma antiga - tratando dados de servidor como estado local
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// E sobre refetch? E cache?
// E se os dados mudarem no servidor?
}Com TanStack Query, dados de servidor se tornam um recurso gerenciado:
import { useQuery } from '@tanstack/react-query';
function ProductList() {
const {
data: products,
isLoading,
error,
refetch
} = useQuery({
queryKey: ['products'],
queryFn: () => fetchProducts(),
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
});
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
return (
<div>
{products?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}O que o TanStack Query oferece automaticamente:
Cache com expiração configurável
Refetch em segundo plano quando os dados ficam obsoletos
Deduplicação de solicitações idênticas
Tentativas automáticas em caso de falha
Estados de loading e erro
Atualizações otimistas
Suporte offline
Padrões Avançados do TanStack Query
Chaves de Query: A Base
Chaves de query são como o TanStack Query sabe quais dados pertencem juntos. Eu as penso como índices de banco de dados para meu cache:
// Chave simples
const productsQuery = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
// Chaves parametrizadas
const productQuery = useQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId)
});
// Chaves complexas com filtros
const filteredProductsQuery = useQuery({
queryKey: ['products', { category, sortBy, search }],
queryFn: () => fetchProducts({ category, sortBy, search })
});Estratégia de Chave que Sigo:
Partes mais específicas no final
Use objetos para parâmetros complexos
Mantenha chaves consistentes na aplicação
Pense em padrões de invalidação
Mutations: Alterando Estado do Servidor
Mutations lidam com operações CREATE, UPDATE, DELETE:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useAddProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newProduct) => createProduct(newProduct),
onMutate: async (newProduct) => {
// Cancelar refetches pendentes
await queryClient.cancelQueries(['products']);
// Capturar valor anterior
const previousProducts = queryClient.getQueryData(['products']);
// Atualização otimista
queryClient.setQueryData(['products'], (old) =>
old ? [...old, { ...newProduct, id: Date.now() }] : [newProduct]
);
return { previousProducts };
},
onError: (err, newProduct, context) => {
// Reverter em caso de erro
queryClient.setQueryData(['products'], context.previousProducts);
},
onSettled: () => {
// Sempre refetch após erro ou sucesso
queryClient.invalidateQueries(['products']);
},
});
}Combinando Zustand e TanStack Query
Aqui é onde a mágica acontece. Usar ambas as bibliotecas juntos cria uma arquitetura poderosa e limpa:
// Estado do cliente no Zustand
const useAppStore = create((set, get) => ({
// Estado da UI
currentView: 'grid',
filters: {
category: null,
priceRange: [0, 1000],
search: '',
},
selectedProducts: [],
// Ações
setView: (view) => set({ currentView: view }),
updateFilters: (newFilters) => set((state) => ({
filters: { ...state.filters, ...newFilters }
})),
toggleProductSelection: (productId) => set((state) => {
const isSelected = state.selectedProducts.includes(productId);
return {
selectedProducts: isSelected
? state.selectedProducts.filter(id => id !== productId)
: [...state.selectedProducts, productId]
};
}),
}));
// Estado do servidor com TanStack Query
function useProducts() {
const filters = useAppStore(state => state.filters);
return useQuery({
queryKey: ['products', filters],
queryFn: () => fetchProducts(filters),
keepPreviousData: true, // Mostrar dados antigos enquanto busca novos
});
}
// Componente usando ambas
function ProductManagement() {
const {
currentView,
filters,
selectedProducts,
setView,
updateFilters,
toggleProductSelection,
} = useAppStore();
const { data: products, isLoading } = useProducts();
return (
<div>
{/* Componentes usando o store e queries */}
</div>
);
}Configuração e Otimização Avançadas
Configuração do Query Client
Configure seu QueryClient com padrões sensatos:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
retry: (failureCount, error) => {
// Não tentar novamente em erros 4xx
if (error.status >= 400 && error.status < 500) {
return false;
}
// Tentar até 3 vezes para outros erros
return failureCount < 3;
},
refetchOnWindowFocus: false,
refetchOnReconnect: 'always',
},
mutations: {
retry: 1,
},
},
});Estratégias de Teste
Ambas as bibliotecas são muito testáveis quando configuradas corretamente:
Testando Stores do Zustand
import { act, renderHook } from '@testing-library/react';
import { useAppStore } from './store';
describe('App Store', () => {
beforeEach(() => {
useAppStore.setState({
items: [],
user: null,
isAuthenticated: false,
});
});
test('deve adicionar item ao carrinho', () => {
const { result } = renderHook(() => useAppStore());
act(() => {
result.current.addItem({ id: 1, name: 'Produto Teste', price: 10 });
});
expect(result.current.items).toHaveLength(1);
expect(result.current.getTotalPrice()).toBe(10);
});
});Testando TanStack Query
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
describe('useProducts', () => {
test('deve buscar produtos com sucesso', async () => {
const mockProducts = [{ id: 1, name: 'Produto Teste' }];
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockProducts),
})
);
const { result } = renderHook(() => useProducts(), {
wrapper: createWrapper(),
});
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockProducts);
});
});Armadilhas Comuns e Como Evitá-las
Armadilhas do Zustand
Mutando Estado Diretamente
// ❌ Não mutate diretamente
const useStore = create((set) => ({
items: [],
addItem: (item) => {
// Isso muta o array original!
get().items.push(item);
}
}));
// ✅ Sempre crie novo estado
const useStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
}))
}));2. Sobrecarregando Estado Global
Nem tudo precisa estar no Zustand. Mantenha estado local quando possível.
Armadilhas do TanStack Query
Não Usando Chaves de Query Adequadamente
// ❌ Chaves estáticas não funcionam com parâmetros
const { data } = useQuery({
queryKey: ['products'],
queryFn: () => fetchProducts(filters), // filters não está na chave!
});
// ✅ Inclua todas as variáveis na chave
const { data } = useQuery({
queryKey: ['products', filters],
queryFn: () => fetchProducts(filters),
});2. Não Lidando com Estados de Loading Adequadamente
Sempre considere a experiência do usuário durante estados de loading e erro.
Exemplo de Arquitetura Real
Aqui está como estruturo uma aplicação React típica com Zustand e TanStack Query:
src/
stores/
useAppStore.js # Store principal do Zustand
slices/
userSlice.js
uiSlice.js
cartSlice.js
hooks/
queries/
useProducts.js # Hooks do TanStack Query
useUsers.js
mutations/
useCreateProduct.js
components/
ProductList.jsx # Usa stores e queries
ShoppingCart.jsx
utils/
queryClient.js # Configuração do query client
api.js # Camada de APIImpacto de Performance: Os Números
No meu último projeto, a mudança de uma configuração Redux + lógica assíncrona personalizada para Zustand + TanStack Query resultou em:
40% de redução no tamanho do bundle (menos código boilerplate)
60% menos re-renders (melhor organização de estado)
Desenvolvimento mais rápido (menos tempo escrevendo boilerplate)
Melhor experiência do usuário (refetch automático em segundo plano, atualizações otimistas)
Embora seus resultados possam variar, a combinação consistentemente entrega melhor performance e experiência de desenvolvedor.
Conclusão
Zustand e TanStack Query mudaram fundamentalmente como abordo o gerenciamento de estado em React. Eles resolvem problemas reais sem introduzir complexidade desnecessária:
Zustand oferece:
Gerenciamento de estado simples e previsível
Sem boilerplate ou cerimônia
Excelente suporte a TypeScript
Poderoso sistema de middleware
TanStack Query oferece:
Gerenciamento inteligente de estado do servidor
Cache e sincronização automáticos
Atualizações otimistas e tratamento de erro
Refetch em segundo plano e gerenciamento de dados obsoletos
Juntos, criam uma solução de gerenciamento de estado que escala de apps simples a aplicações enterprise complexas.
Takeaway: Esta combinação representa o estado atual da arte em gerenciamento de estado para React - simples o suficiente para projetos pequenos, mas poderosa o suficiente para aplicações complexas, sem a complexidade desnecessária de soluções tradicionais.
