Zustand
Stores globais leves com o mínimo de boilerplate. Zustand se encaixa em estado de UI do cliente que sobrevive a mudanças de tela - carrinho, preferências, conclusão de tour de recursos. Emparelhe com TanStack Query para dados de API; não duplique caches de servidor em Zustand.
Cartão de receita de referência rápida - pronto para copiar e colar.
// src/features/cart/cartStore.ts
import { create } from "zustand" ;
type CartItem = { productId : string ; qty : number };
type CartState = {
items : CartItem [];
add : ( item : CartItem ) => void ;
remove : ( productId : string ) => void ;
totalQty : () => number ;
};
export const useCartStore = create < CartState >(( set , get ) => ({
items: [],
add : ( item ) =>
set (( s ) => {
const existing = s.items. find (( i ) => i.productId === item.productId);
if (existing) {
return {
items: s.items. map (( i ) =>
i.productId === item.productId ? { ... i, qty: i.qty + item.qty } : i
),
};
}
return { items: [ ... s.items, item] };
}),
remove : ( productId ) => set (( s ) => ({ items: s.items. filter (( i ) => i.productId !== productId) })),
totalQty : () => get ().items. reduce (( n , i ) => n + i.qty, 0 ),
}));
// Consumidor - selector limita re-renderizações
import { useCartStore } from "@/features/cart/cartStore" ;
const qty = useCartStore (( s ) => s. totalQty ());
Quando usar isso:
Múltiplas telas leem/escrevem o mesmo estado do cliente (badge do carrinho, filtros persistidos localmente).
Contexto causa tempestades de re-renderização visíveis no profiler - selectors corrigem isso.
Você quer um bundle pequeno (~1KB) sem a cerimônia do Redux.
Atualizações de estado são UI síncrona - não cache de servidor com tempos de expiração.
Padrão da equipe do ADR para UI global do cliente.
// stores/preferencesStore.ts - padrão de slice + devtools em dev
import { create } from "zustand" ;
import { devtools } from "zustand/middleware" ;
type ThemeMode = "system" | "light" | "dark" ;
type PreferencesState = {
theme : ThemeMode ;
hapticsEnabled : boolean ;
setTheme : ( theme : ThemeMode ) => void ;
toggleHaptics : () => void ;
};
export const usePreferencesStore = create < PreferencesState >()(
devtools (
( set ) => ({
theme: "system" ,
hapticsEnabled: true ,
setTheme : ( theme ) => set ({ theme }, false , "preferences/setTheme" ),
toggleHaptics : () => set (( s ) => ({ hapticsEnabled: ! s.hapticsEnabled }), false , "preferences/toggleHaptics" ),
}),
{ name: "PreferencesStore" , enabled: __DEV__ }
)
);
// stores/cartStore.ts - store de domínio separado
import { create } from "zustand" ;
type CartItem = { productId : string ; title : string ; qty : number };
type CartState = {
items : CartItem [];
add : ( item : CartItem ) => void ;
clear : () => void ;
};
export const useCartStore = create < CartState >(( set ) => ({
items: [],
add : ( item ) => set (( s ) => ({ items: [ ... s.items, item] })),
clear : () => set ({ items: [] }),
}));
// --- UI ---
import * as Haptics from "expo-haptics" ;
import { Pressable, Text, View } from "react-native" ;
export function SettingsScreen () {
const theme = usePreferencesStore (( s ) => s.theme);
const hapticsEnabled = usePreferencesStore (( s ) => s.hapticsEnabled);
const setTheme = usePreferencesStore (( s ) => s.setTheme);
const toggleHaptics = usePreferencesStore (( s ) => s.toggleHaptics);
function onToggleHaptics () {
if (hapticsEnabled) void Haptics. selectionAsync ();
toggleHaptics ();
}
return (
< View style = {{ padding: 16 , gap: 12 }}>
< Text >Tema: {theme}</ Text >
< Pressable onPress = {() => setTheme (theme === "dark" ? "light" : "dark" )}>
< Text >Alternar tema</ Text >
</ Pressable >
< Pressable onPress = {onToggleHaptics}>
< Text >Haptics: {hapticsEnabled ? "On" : "Off" }</ Text >
</ Pressable >
</ View >
);
}
export function CartBadge () {
const count = useCartStore (( s ) => s.items. reduce (( n , i ) => n + i.qty, 0 ));
return < Text >Carrinho ({count})</ Text >;
}
// cartStore.test.ts
import { useCartStore } from "./cartStore" ;
const initial = useCartStore. getState ();
beforeEach (() => {
useCartStore. setState (initial, true );
});
test ( "adiciona mescla quantidade" , () => {
useCartStore. getState (). add ({ productId: "1" , title: "Pacote" , qty: 1 });
useCartStore. getState (). add ({ productId: "1" , title: "Pacote" , qty: 2 });
expect (useCartStore. getState ().items). toHaveLength ( 2 ); // ou política de mesclagem por regras de produto
});
O que isso demonstra:
Stores separados por domínio - mudanças de preferências não afetam os assinantes do carrinho.
Middleware devtools nomeia ações para Redux DevTools apenas em desenvolvimento.
Selectors em useCartStore mantêm as atualizações do badge da aba baratas.
setState(..., true) substitui todo o estado em testes - reset determinístico.
Haptics ficam no componente - o store contém flags, não efeitos colaterais nativos.
Combine slices relacionados quando eles compartilham o namespace devtools:
import { create } from "zustand" ;
type BearSlice = { bears : number ; addBear : () => void };
type FishSlice = { fish : number ; addFish : () => void };
type Store = BearSlice & FishSlice ;
export const useBoundStore = create < Store >()(( ... a ) => ({
... createBearSlice ( ... a),
... createFishSlice ( ... a),
}));
function createBearSlice ( set : Parameters < typeof create< BearSlice >>[ 0 ]) : BearSlice {
return {
bears: 0 ,
addBear : () => set (( s ) => ({ bears: s.bears + 1 })),
};
}
Prefira múltiplos stores create() até que transações entre slices sejam necessárias.
import AsyncStorage from "@react-native-async-storage/async-storage" ;
import { create } from "zustand" ;
import { createJSONStorage, persist } from "zustand/middleware" ;
export const usePreferencesStore = create (
persist < PreferencesState >(
( set ) => ({
theme: "system" ,
hapticsEnabled: true ,
setTheme : ( theme ) => set ({ theme }),
toggleHaptics : () => set (( s ) => ({ hapticsEnabled: ! s.hapticsEnabled })),
}),
{
name: "preferences-v1" ,
storage: createJSONStorage (() => AsyncStorage),
partialize : ( s ) => ({ theme: s.theme, hapticsEnabled: s.hapticsEnabled }),
}
)
);
partialize exclui ações do disco - serializa apenas campos de dados.
Nunca persista tokens - use expo-secure-store via um adaptador de armazenamento personalizado - veja Persistência e Hidratação de Estado .
Incremente name (preferences-v2) quando o esquema mudar - ou execute a migração em onRehydrateStorage.
O middleware devtools envia ações para a Extensão Redux DevTools ao depurar JS via Flipper ou desktop remoto. Desabilite em produção: enabled: __DEV__.
import { subscribeWithSelector } from "zustand/middleware" ;
useCartStore. subscribe (
( s ) => s.items. length ,
( len ) => console. log ( "tamanho do carrinho" , len)
);
Use para efeitos colaterais de análise - não para renders React.
import { immer } from "zustand/middleware/immer" ;
create < CartState >()(
immer (( set ) => ({
items: [],
add : ( item ) =>
set (( state ) => {
state.items. push (item);
}),
}))
);
Selecionando o store inteiro - useCartStore() sem selector re-renderiza a cada mudança no carrinho. Correção: useCartStore((s) => s.items.length).
Dados derivados no store sem memoização - get().items.map(...) em um render via selector é executado a cada vez. Correção: Selector computado ou useMemo no resultado.
Armazenando respostas de API - A invalidação de cache se torna manual. Correção: TanStack Query.
Efeitos colaterais dentro de set - Chamar fetch em add() acopla o store à rede. Correção: Componente ou thunk chama a API, depois set.
Um store gigante - Ciclos de importação e problemas de hot reload. Correção: features/cart/cartStore.ts, features/settings/preferencesStore.ts.
Persistir tudo - JSON grande do carrinho atrasa o início frio. Correção: partialize, TTL, ou persistência de Query para leituras de servidor.
Esquecer o reset de teste - Testes vazam estado dependente da ordem. Correção: beforeEach(() => useCartStore.setState(initial, true)).
Alternativa Usar Quando Não Usar Quando Zustand UI global do cliente + selectors Cache de lista do servidor Context (dividido) Tema/sessão, zero dependências Atualizações de carrinho de alta frequência Redux Toolkit Auditoria + ecossistema de middleware Greenfield apenas com carrinho + tema Jotai Grafos de UI derivados atômicos Equipe não familiarizada com atoms MMKV raw Persistência crítica de velocidade Você ainda precisa de semântica de atualização
Zustand precisa de um Provider?
Não - stores são singletons de módulo. Múltiplas raízes React (raro em RN) compartilharão o mesmo store, a menos que você passe createStore via context.
Zustand vs Redux Toolkit em 2026?
Zustand padrão para estado global do cliente. Redux quando DevTools, middleware e padrões organizacionais exigem reducers centralizados - veja Redux Toolkit .
Como usar Zustand com Expo Router?
Importe hooks em qualquer arquivo de rota. Orquestre a navegação na rota; o store contém os dados. Evite importar router dentro de módulos de store.
Zustand pode substituir TanStack Query?
Não para estado do servidor. Zustand pode manter flags de UI otimistas; Query possui as entidades buscadas e chaves de cache.
Qual o impacto no bundle?
Zustand tem ~1–3KB gzipped - insignificante comparado a um único asset de imagem. Redux Toolkit é maior, mas inclui RTK Query.
Padrão de ações assíncronas?
Mantenha o assíncrono em hooks ou serviços:
async function checkout () {
set ({ status: "pending" });
try {
await api. checkout ( get ().items);
set ({ status: "done" , items: [] });
} catch (e) {
set ({ status: "error" });
}
}
Melhor prática TypeScript?
Defina o tipo CartState explicitamente; use create<CartState>()(...) com middleware para tipos de middleware inferidos.
Como hidratar ao iniciar o app?
Use persist + callback onRehydrateStorage para controlar a UI - veja Persistência e Hidratação de Estado .
Múltiplos componentes com o mesmo selector?
Zustand usa Object.is no resultado do selector - retorne primitivos ou referências estáveis para evitar renders extras.
Integração com React Compiler?
Selectors ainda são recomendados - a memoização do compilador não substitui as subscrições de store entre componentes.
Limpeza de logout?
useCartStore.setState({ items: [] }) mais queryClient.clear() - caches do cliente e do servidor devem ser limpos juntos.
Posso usar Zustand com immer e devtools juntos?
Sim - a ordem do middleware importa: devtools(persist(immer(...))) - verifique a documentação do Zustand para sua cadeia de middleware.
Versões da Stack: Esta página foi escrita para React 19.2.3 , React Native 0.86.0 e Expo SDK 57 (expo ~57.0.4).