Noções Básicas de Gerenciamento de Estado
10 exemplos para você começar com Gerenciamento de Estado - 7 básicos e 3 intermediários. Usuários de dispositivos móveis perdem sinal constantemente; classifique o estado antes de escolher uma biblioteca.
Busque em todas as páginas da documentação
10 exemplos para você começar com Gerenciamento de Estado - 7 básicos e 3 intermediários. Usuários de dispositivos móveis perdem sinal constantemente; classifique o estado antes de escolher uma biblioteca.
Crie um aplicativo Expo com formato de produção e um pin explícito do SDK 57. Os exemplos de estado assumem Expo Router e TypeScript.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npm installConfirme o pin do SDK antes de adicionar bibliotecas de estado:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Instale a camada padrão de estado do servidor que a maioria das equipes adota desde o primeiro dia:
npx expo install @tanstack/react-query @react-native-community/netinfoFerramentas: Estes exemplos visam o Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3.
Modal aberto, aba expandida, índice de aba pressionado - estado que morre quando a tela é desmontada pertence ao useState dessa tela.
// app/(tabs)/catalog/index.tsx
import { useState } from "react";
import { Modal, Pressable, Text, View } from "react-native";
export default function CatalogScreen() {
const [filterOpen, setFilterOpen] = useState(false);
return (
<View style={{ flex: 1, padding: 16 }}>
<Pressable onPress={() => setFilterOpen(true)}>
<Text>Filtros</Text>
</Pressable>
<Modal visible={filterOpen} animationType="slide" onRequestClose={() => setFilterOpen(false)}>
<View style={{ flex: 1, padding: 24 }}>
<Text>Aba de filtros - apenas estado local</Text>
<Pressable onPress={() => setFilterOpen(false)}>
<Text>Fechar</Text>
</Pressable>
</View>
</Modal>
</View>
);
}filterOpen não precisa de Redux, Zustand ou Context - nenhuma outra tela o lêRelacionado: useState & useReducer - quando o estado local excede uma única tela
Props fluem para baixo; estado é de propriedade do componente que o atualiza. Itens de lista de dispositivos móveis devem permanecer apresentacionais.
// src/features/catalog/components/ProductRow.tsx
import { Pressable, Text, View } from "react-native";
type Props = {
title: string;
priceLabel: string;
selected: boolean;
onPress: () => void;
};
export function ProductRow({ title, priceLabel, selected, onPress }: Props) {
return (
<Pressable onPress={onPress} style={{ padding: 12, backgroundColor: selected ? "#e0f2fe" : "#fff" }}>
<Text style={{ fontWeight: "600" }}>{title}</Text>
<Text>{priceLabel}</Text>
</Pressable>
);
}// O pai é o dono do estado de seleção
import { useState } from "react";
import { FlatList } from "react-native";
import { ProductRow } from "@/features/catalog/components/ProductRow";
const PRODUCTS = [
{ id: "1", title: "Trail Pack", priceLabel: "$89" },
{ id: "2", title: "Day Pack", priceLabel: "$49" },
];
export function ProductList() {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<FlatList
data={PRODUCTS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProductRow
title={item.title}
priceLabel={item.priceLabel}
selected={selectedId === item.id}
onPress={() => setSelectedId(item.id)}
/>
)}
/>
);
}ProductRow não tem useState - re-renderiza apenas quando as props mudamselectedId do pai é a única fonte de verdade para o estado de destaqueonPress mantém a navegação e os efeitos colaterais na camada do containerRelacionado: ../component-patterns/container-presenter-on-mobile/container-presenter-on-mobile.md - separando hooks de dados de JSX
Estado do servidor vem de uma API e fica desatualizado. Estado do cliente é a UI que o usuário controla. Misturá-los em um único blob useState causa bugs de re-fetch.
// ❌ Anti-padrão - dados da API em useState com useEffect manual
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/orders")
.then((r) => r.json())
.then(setOrders)
.finally(() => setLoading(false));
}, []);// ✅ Estado do servidor no TanStack Query; estado do cliente permanece local
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
function OrdersScreen() {
const [sort, setSort] = useState<"newest" | "oldest">("newest");
const { data: orders = [], isPending, isError, refetch } = useQuery({
queryKey: ["orders"],
queryFn: fetchOrders,
staleTime: 60_000,
});
const sorted = [...orders].sort((a, b) =>
sort === "newest" ? b.placedAt.localeCompare(a.placedAt) : a.placedAt.localeCompare(b.placedAt)
);
// renderizar sorted, isPending, isError, refetch, setSort...
}orders são de propriedade do servidor - cache, deduplicação e atualização em segundo plano pertencem ao Querysort é de propriedade do cliente - sem ida e volta à rede quando o usuário alternarefetch() - não um setOrders manualRelacionado: TanStack Query - políticas de cache para dispositivos móveis | ../architecture-design/adr-state-management-selection/adr-state-management-selection.md - matriz de decisão
Badge do carrinho, modo de tema, conclusão do onboarding - estado que sobrevive à navegação mas não são dados de API se encaixam em um pequeno store global ou Context dividido.
// src/features/cart/useCartStore.ts
import { create } from "zustand";
type CartItem = { productId: string; qty: number };
type CartStore = {
items: CartItem[];
add: (item: CartItem) => void;
count: () => number;
};
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
count: () => get().items.reduce((n, i) => n + i.qty, 0),
}));// A barra de abas lê a contagem via seletor - não o store inteiro
import { useCartStore } from "@/features/cart/useCartStore";
import { Text, View } from "react-native";
export function CartTabIcon() {
const count = useCartStore((s) => s.count());
return (
<View>
<Text>Carrinho</Text>
{count > 0 && <Text>{count}</Text>}
</View>
);
}npx expo install zustand - JS puro, sem módulo nativoRelacionado: Zustand - slices, devtools e testes
Filtros, abas e paginação que devem ser restaurados em deep links pertencem aos parâmetros de pesquisa da rota - não a um store global.
// app/(tabs)/catalog/index.tsx
import { router, useLocalSearchParams } from "expo-router";
import { Pressable, Text, View } from "react-native";
type Params = { category?: string };
export default function CatalogRoute() {
const { category = "all" } = useLocalSearchParams<Params>();
return (
<View style={{ padding: 16, gap: 8 }}>
<Text>Categoria: {category}</Text>
{(["all", "gear", "apparel"] as const).map((c) => (
<Pressable key={c} onPress={() => router.setParams({ category: c })}>
<Text style={{ fontWeight: category === c ? "700" : "400" }}>{c}</Text>
</Pressable>
))}
</View>
);
}useLocalSearchParams lê a string de consulta equivalente da rota atualrouter.setParams atualiza os parâmetros sem perder a posição da pilha - o botão voltar do sistema operacional restaura o filtro anteriormyapp://catalog?category=gear) funcionam quando scheme é definido em app.config.tsRelacionado: Rotas Tipadas - parâmetros tipados no SDK 57
Em dispositivos móveis, assuma que o usuário está offline em um elevador. Mostre os últimos dados válidos com um banner offline sutil - não uma tela em branco.
import NetInfo from "@react-native-community/netinfo";
import { onlineManager, useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { Text, View } from "react-native";
onlineManager.setEventListener((setOnline) =>
NetInfo.addEventListener((state) => setOnline(!!state.isConnected))
);
function ProductList() {
const { data, isPending, isFetching, isError } = useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
staleTime: 5 * 60_000,
gcTime: 24 * 60 * 60_000,
retry: 2,
});
const showOfflineCache = !isPending && data && isError;
return (
<View>
{showOfflineCache && <Text>Offline - mostrando resultados salvos</Text>}
{isFetching && !isPending && <Text>Atualizando…</Text>}
{/* renderizar dados */}
</View>
);
}staleTime mantém os dados em cache visíveis enquanto uma re-busca em segundo plano é executadaonlineManager informa ao Query quando o dispositivo se reconecta - aciona refetchOnReconnectgcTime (anteriormente cacheTime) controla quanto tempo o cache não utilizado sobrevive na memóriaRelacionado: TanStack Query -
focusManagerno AppState | ../error-resilience/network-failure-ux/network-failure-ux.md - padrões de UX offline
Conecte provedores de estado do servidor e de sessão uma vez em app/_layout.tsx - evite aninhar provedores por tela.
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Stack } from "expo-router";
import { useState } from "react";
import { SessionProvider } from "@/features/auth/SessionProvider";
export default function RootLayout() {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, retry: 2 },
},
})
);
return (
<QueryClientProvider client={queryClient}>
<SessionProvider>
<Stack screenOptions={{ headerShown: false }} />
</SessionProvider>
</QueryClientProvider>
);
}QueryClient é criado uma vez por sessão do aplicativo - useState(() => new QueryClient()) evita compartilhamento acidental entre testes e produção no mesmo móduloSessionProvider expõe a identidade de autenticação - tokens vivem no expo-secure-store, não no cache do QueryRelacionado: Context Without Storms - divida contextos quando os provedores re-renderizam demais
Uma tela de checkout combina carrinho (cliente), opções de frete (servidor) e método selecionado (cliente). Três baldes, três ferramentas.
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useCartStore } from "@/features/cart/useCartStore";
export function CheckoutScreen() {
const items = useCartStore((s) => s.items);
const [shippingId, setShippingId] = useState<string | null>(null);
const { data: methods = [], isPending } = useQuery({
queryKey: ["shipping-methods", items.length],
queryFn: () => fetchShippingMethods(items),
enabled: items.length > 0,
});
// UI: resumo do carrinho do Zustand, métodos do Query, seleção do useState
}items.length para invalidaçãoshippingId é UI efêmera nesta tela - useState até o envio, depois mutaçãoRelacionado: ../architecture-design/clean-architecture-on-mobile/clean-architecture-on-mobile.md - casos de uso vs. hooks
Persista o cache do Query no AsyncStorage para que as telas de catálogo renderizem imediatamente após o encerramento do processo - hidrate antes da primeira pintura, quando possível.
// src/lib/queryPersister.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createAsyncStoragePersister } from "@tanstack/react-query-async-storage-persister";
export const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: "REACT_QUERY_OFFLINE_CACHE",
});// app/_layout.tsx (trecho)
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { asyncStoragePersister } from "@/lib/queryPersister";
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister, maxAge: 1000 * 60 * 60 * 24 }}
>
{children}
</PersistQueryClientProvider>npx expo install @react-native-async-storage/async-storage @tanstack/react-query-persist-client @tanstack/query-async-storage-persistermaxAge limita a idade do cache persistido - combine com staleTime por consultaqueryClient.clear() e limpar o persister - dados de usuário desatualizados são um bug de segurançaRelacionado: Persistência e Hidratação de Estado - início a frio sem travamentos
Execute este checklist quando um colega propuser Redux, Jotai ou outro store.
Checklist de estado (responda antes de npm install):
1. É de uma API? → TanStack Query (ou RTK Query se já usar Redux)
2. É compartilhável em uma URL? → Parâmetros de pesquisa do Expo Router
3. É um formulário com múltiplos campos? → React Hook Form + mutação no submit
4. É UI global do cliente? → Zustand ou Context dividido
5. É efêmero em uma tela? → useState / useReducer
6. É um token secreto? → expo-secure-store + SessionProvider fino
7. A conformidade precisa de auditoria? → Redux Toolkit + DevTools
Se duas bibliotecas responderem "sim" à mesma pergunta - escolha uma e documente em um ADR.// Documente o padrão da equipe em um comentário ou link ADR no limite do store
/** @see docs/adr-state-management - servidor: Query, cliente: Zustand, URL: Router */
export const usePreferences = create<PreferencesStore>(/* ... */);Relacionado: ../architecture-design/adr-state-management-selection/adr-state-management-selection.md - decisões classificadas | Melhores Práticas - resumo da seção
Versões da pilha: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026