O que é Context API e quando usar
A Context API é um mecanismo do React para compartilhar dados “globais” (ou amplamente reutilizados) entre componentes sem precisar passar props por muitas camadas. Em apps React Native pequenos e médios, ela costuma ser um padrão suficiente para: autenticação (usuário logado), preferências (tema/idioma), carrinho simples, feature flags e configurações de app.
O ponto central: quando o valor de um Context muda, todos os componentes que consomem esse Context podem re-renderizar. Por isso, além de criar o Context, é importante desenhar a arquitetura para reduzir renders desnecessários (por exemplo, separando Context de estado e Context de ações).
Boas práticas que vamos aplicar
- Provider único por domínio (ex.: Auth, Preferences, Cart) ou um Provider raiz que compõe vários.
- useReducer para centralizar atualizações e evitar “setState espalhado”.
- Estado derivado calculado com
useMemo(ex.: total do carrinho, se está autenticado). - Separar Context de estado e Context de ações para reduzir renders em componentes que só disparam ações.
- Persistência + reidratação na inicialização para manter sessão e preferências.
Estrutura sugerida de pastas (foco em Context)
Uma organização simples e escalável é agrupar por “domínio”:
src/contexts/ auth/ AuthProvider.tsx authReducer.ts authStorage.ts types.ts preferences/ PreferencesProvider.tsx preferencesReducer.ts preferencesStorage.ts types.ts cart/ CartProvider.tsx cartReducer.ts cartStorage.ts types.ts AppProviders.tsxIsso evita um “Context gigante” e mantém responsabilidades claras.
Persistência: AsyncStorage e reidratação
Para persistir estado localmente em React Native, uma opção comum é @react-native-async-storage/async-storage. A ideia é: ao iniciar o app, ler o estado salvo (rehydrate) e popular o reducer; e, sempre que o estado mudar, salvar novamente (persist).
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Instalação (referência):
npm i @react-native-async-storage/async-storageExemplo real: Auth + Preferências + Carrinho
A seguir, vamos montar três domínios com Context + Provider, cada um com: reducer, ações, estado derivado e persistência.
1) Autenticação (usuário autenticado)
Tipos e estado inicial
// src/contexts/auth/types.ts
export type User = {
id: string;
name: string;
email: string;
};
export type AuthState = {
user: User | null;
token: string | null;
isHydrated: boolean; // indica se já reidratou do storage
};
export type AuthAction =
| { type: 'HYDRATE'; payload: { user: User | null; token: string | null } }
| { type: 'SIGN_IN_SUCCESS'; payload: { user: User; token: string } }
| { type: 'SIGN_OUT' };
export const authInitialState: AuthState = {
user: null,
token: null,
isHydrated: false,
};Reducer simples
// src/contexts/auth/authReducer.ts
import { AuthAction, AuthState, authInitialState } from './types';
export function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'HYDRATE':
return {
...state,
user: action.payload.user,
token: action.payload.token,
isHydrated: true,
};
case 'SIGN_IN_SUCCESS':
return {
...state,
user: action.payload.user,
token: action.payload.token,
};
case 'SIGN_OUT':
return { ...authInitialState, isHydrated: true };
default:
return state;
}
}Storage (persist/rehydrate)
// src/contexts/auth/authStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { User } from './types';
const AUTH_KEY = '@app/auth';
type PersistedAuth = {
user: User | null;
token: string | null;
};
export async function loadAuth(): Promise<PersistedAuth> {
const raw = await AsyncStorage.getItem(AUTH_KEY);
if (!raw) return { user: null, token: null };
try {
return JSON.parse(raw) as PersistedAuth;
} catch {
return { user: null, token: null };
}
}
export async function saveAuth(data: PersistedAuth): Promise<void> {
await AsyncStorage.setItem(AUTH_KEY, JSON.stringify(data));
}
export async function clearAuth(): Promise<void> {
await AsyncStorage.removeItem(AUTH_KEY);
}Separando Context de estado e de ações
Criaremos dois contexts: um para estado e outro para ações. Assim, um componente que só chama signOut() não precisa re-renderizar quando o usuário muda (desde que ele não consuma o estado).
// src/contexts/auth/AuthProvider.tsx
import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
import { authReducer } from './authReducer';
import { AuthState, authInitialState, User } from './types';
import { clearAuth, loadAuth, saveAuth } from './authStorage';
type AuthActions = {
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
};
const AuthStateContext = createContext<AuthState | null>(null);
const AuthActionsContext = createContext<AuthActions | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(authReducer, authInitialState);
// Reidratação na inicialização
useEffect(() => {
let mounted = true;
(async () => {
const persisted = await loadAuth();
if (!mounted) return;
dispatch({ type: 'HYDRATE', payload: persisted });
})();
return () => {
mounted = false;
};
}, []);
// Persistência sempre que user/token mudarem (após hidratar)
useEffect(() => {
if (!state.isHydrated) return;
saveAuth({ user: state.user, token: state.token });
}, [state.user, state.token, state.isHydrated]);
const signIn = useCallback(async (email: string, password: string) => {
// Exemplo didático: simular login. Em app real, chamar API.
await new Promise((r) => setTimeout(r, 400));
const fakeUser: User = { id: 'u1', name: 'Ada Lovelace', email };
const fakeToken = 'token_abc123';
dispatch({ type: 'SIGN_IN_SUCCESS', payload: { user: fakeUser, token: fakeToken } });
}, []);
const signOut = useCallback(async () => {
dispatch({ type: 'SIGN_OUT' });
await clearAuth();
}, []);
const actions = useMemo<AuthActions>(() => ({ signIn, signOut }), [signIn, signOut]);
return (
<AuthStateContext.Provider value={state}>
<AuthActionsContext.Provider value={actions}>
{children}
</AuthActionsContext.Provider>
</AuthStateContext.Provider>
);
}
export function useAuthState() {
const ctx = useContext(AuthStateContext);
if (!ctx) throw new Error('useAuthState deve ser usado dentro de AuthProvider');
return ctx;
}
export function useAuthActions() {
const ctx = useContext(AuthActionsContext);
if (!ctx) throw new Error('useAuthActions deve ser usado dentro de AuthProvider');
return ctx;
}
// Estado derivado (ex.: isAuthenticated)
export function useIsAuthenticated() {
const { token } = useAuthState();
return Boolean(token);
}2) Preferências (tema e idioma)
Preferências são um ótimo caso para Context. Vamos manter theme e language, com ações para alternar e persistência.
// src/contexts/preferences/types.ts
export type ThemeMode = 'light' | 'dark';
export type Language = 'pt-BR' | 'en-US';
export type PreferencesState = {
theme: ThemeMode;
language: Language;
isHydrated: boolean;
};
export type PreferencesAction =
| { type: 'HYDRATE'; payload: { theme: ThemeMode; language: Language } }
| { type: 'SET_THEME'; payload: ThemeMode }
| { type: 'SET_LANGUAGE'; payload: Language };
export const preferencesInitialState: PreferencesState = {
theme: 'light',
language: 'pt-BR',
isHydrated: false,
};// src/contexts/preferences/preferencesReducer.ts
import { PreferencesAction, PreferencesState } from './types';
export function preferencesReducer(
state: PreferencesState,
action: PreferencesAction
): PreferencesState {
switch (action.type) {
case 'HYDRATE':
return { ...state, ...action.payload, isHydrated: true };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'SET_LANGUAGE':
return { ...state, language: action.payload };
default:
return state;
}
}// src/contexts/preferences/preferencesStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Language, ThemeMode } from './types';
const PREFS_KEY = '@app/preferences';
type PersistedPrefs = { theme: ThemeMode; language: Language };
export async function loadPreferences(): Promise<PersistedPrefs> {
const raw = await AsyncStorage.getItem(PREFS_KEY);
if (!raw) return { theme: 'light', language: 'pt-BR' };
try {
return JSON.parse(raw) as PersistedPrefs;
} catch {
return { theme: 'light', language: 'pt-BR' };
}
}
export async function savePreferences(data: PersistedPrefs): Promise<void> {
await AsyncStorage.setItem(PREFS_KEY, JSON.stringify(data));
}// src/contexts/preferences/PreferencesProvider.tsx
import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
import { preferencesReducer } from './preferencesReducer';
import { preferencesInitialState, PreferencesState, ThemeMode, Language } from './types';
import { loadPreferences, savePreferences } from './preferencesStorage';
type PreferencesActions = {
setTheme: (mode: ThemeMode) => void;
toggleTheme: () => void;
setLanguage: (lang: Language) => void;
};
const PreferencesStateContext = createContext<PreferencesState | null>(null);
const PreferencesActionsContext = createContext<PreferencesActions | null>(null);
export function PreferencesProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(preferencesReducer, preferencesInitialState);
useEffect(() => {
let mounted = true;
(async () => {
const persisted = await loadPreferences();
if (!mounted) return;
dispatch({ type: 'HYDRATE', payload: persisted });
})();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (!state.isHydrated) return;
savePreferences({ theme: state.theme, language: state.language });
}, [state.theme, state.language, state.isHydrated]);
const setTheme = useCallback((mode: ThemeMode) => {
dispatch({ type: 'SET_THEME', payload: mode });
}, []);
const toggleTheme = useCallback(() => {
dispatch({ type: 'SET_THEME', payload: state.theme === 'light' ? 'dark' : 'light' });
}, [state.theme]);
const setLanguage = useCallback((lang: Language) => {
dispatch({ type: 'SET_LANGUAGE', payload: lang });
}, []);
const actions = useMemo(() => ({ setTheme, toggleTheme, setLanguage }), [setTheme, toggleTheme, setLanguage]);
return (
<PreferencesStateContext.Provider value={state}>
<PreferencesActionsContext.Provider value={actions}>
{children}
</PreferencesActionsContext.Provider>
</PreferencesStateContext.Provider>
);
}
export function usePreferencesState() {
const ctx = useContext(PreferencesStateContext);
if (!ctx) throw new Error('usePreferencesState deve ser usado dentro de PreferencesProvider');
return ctx;
}
export function usePreferencesActions() {
const ctx = useContext(PreferencesActionsContext);
if (!ctx) throw new Error('usePreferencesActions deve ser usado dentro de PreferencesProvider');
return ctx;
}3) Carrinho simples (itens, quantidade, total derivado)
O carrinho é um exemplo clássico de estado global. Vamos manter itens com quantidade, ações para adicionar/remover/limpar e um total derivado.
// src/contexts/cart/types.ts
export type CartItem = {
id: string;
title: string;
price: number;
quantity: number;
};
export type CartState = {
items: CartItem[];
isHydrated: boolean;
};
export type CartAction =
| { type: 'HYDRATE'; payload: { items: CartItem[] } }
| { type: 'ADD_ITEM'; payload: { id: string; title: string; price: number } }
| { type: 'REMOVE_ITEM'; payload: { id: string } }
| { type: 'SET_QTY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR' };
export const cartInitialState: CartState = {
items: [],
isHydrated: false,
};// src/contexts/cart/cartReducer.ts
import { CartAction, CartState } from './types';
export function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'HYDRATE':
return { ...state, items: action.payload.items, isHydrated: true };
case 'ADD_ITEM': {
const existing = state.items.find((i) => i.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map((i) =>
i.id === action.payload.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return {
...state,
items: [
...state.items,
{ id: action.payload.id, title: action.payload.title, price: action.payload.price, quantity: 1 },
],
};
}
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter((i) => i.id !== action.payload.id) };
case 'SET_QTY':
return {
...state,
items: state.items
.map((i) => (i.id === action.payload.id ? { ...i, quantity: action.payload.quantity } : i))
.filter((i) => i.quantity > 0),
};
case 'CLEAR':
return { ...state, items: [] };
default:
return state;
}
}// src/contexts/cart/cartStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CartItem } from './types';
const CART_KEY = '@app/cart';
type PersistedCart = { items: CartItem[] };
export async function loadCart(): Promise<PersistedCart> {
const raw = await AsyncStorage.getItem(CART_KEY);
if (!raw) return { items: [] };
try {
return JSON.parse(raw) as PersistedCart;
} catch {
return { items: [] };
}
}
export async function saveCart(data: PersistedCart): Promise<void> {
await AsyncStorage.setItem(CART_KEY, JSON.stringify(data));
}// src/contexts/cart/CartProvider.tsx
import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
import { cartReducer } from './cartReducer';
import { cartInitialState, CartState } from './types';
import { loadCart, saveCart } from './cartStorage';
type CartActions = {
addItem: (item: { id: string; title: string; price: number }) => void;
removeItem: (id: string) => void;
setQty: (id: string, quantity: number) => void;
clear: () => void;
};
const CartStateContext = createContext<CartState | null>(null);
const CartActionsContext = createContext<CartActions | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, cartInitialState);
useEffect(() => {
let mounted = true;
(async () => {
const persisted = await loadCart();
if (!mounted) return;
dispatch({ type: 'HYDRATE', payload: persisted });
})();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (!state.isHydrated) return;
saveCart({ items: state.items });
}, [state.items, state.isHydrated]);
const addItem = useCallback((item: { id: string; title: string; price: number }) => {
dispatch({ type: 'ADD_ITEM', payload: item });
}, []);
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: { id } });
}, []);
const setQty = useCallback((id: string, quantity: number) => {
dispatch({ type: 'SET_QTY', payload: { id, quantity } });
}, []);
const clear = useCallback(() => {
dispatch({ type: 'CLEAR' });
}, []);
const actions = useMemo(() => ({ addItem, removeItem, setQty, clear }), [addItem, removeItem, setQty, clear]);
return (
<CartStateContext.Provider value={state}>
<CartActionsContext.Provider value={actions}>
{children}
</CartActionsContext.Provider>
</CartStateContext.Provider>
);
}
export function useCartState() {
const ctx = useContext(CartStateContext);
if (!ctx) throw new Error('useCartState deve ser usado dentro de CartProvider');
return ctx;
}
export function useCartActions() {
const ctx = useContext(CartActionsContext);
if (!ctx) throw new Error('useCartActions deve ser usado dentro de CartProvider');
return ctx;
}
// Estado derivado: total e quantidade total
export function useCartSummary() {
const { items } = useCartState();
return useMemo(() => {
const totalItems = items.reduce((acc, i) => acc + i.quantity, 0);
const totalPrice = items.reduce((acc, i) => acc + i.quantity * i.price, 0);
return { totalItems, totalPrice };
}, [items]);
}Compondo Providers no app
Para não “anilhar” providers no App.tsx de forma desorganizada, crie um componente agregador.
// src/contexts/AppProviders.tsx
import React from 'react';
import { AuthProvider } from './auth/AuthProvider';
import { PreferencesProvider } from './preferences/PreferencesProvider';
import { CartProvider } from './cart/CartProvider';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<PreferencesProvider>
<CartProvider>{children}</CartProvider>
</PreferencesProvider>
</AuthProvider>
);
}Uso:
// App.tsx
import React from 'react';
import { AppProviders } from './src/contexts/AppProviders';
import { Root } from './src/Root';
export default function App() {
return (
<AppProviders>
<Root />
</AppProviders>
);
}Passo a passo prático: consumindo estado e ações
Tela de perfil (mostra usuário e botão de sair)
Note como o botão de sair pode consumir apenas ações, reduzindo renders quando o estado muda.
// src/screens/ProfileScreen.tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useAuthState, useAuthActions } from '../contexts/auth/AuthProvider';
export function ProfileScreen() {
const { user, isHydrated } = useAuthState();
const { signOut } = useAuthActions();
if (!isHydrated) return <Text>Carregando...</Text>;
return (
<View>
<Text>{user ? `Olá, ${user.name}` : 'Você não está logado'}</Text>
{user ? <Button title="Sair" onPress={signOut} /> : null}
</View>
);
}Tela de configurações (tema e idioma)
// src/screens/SettingsScreen.tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { usePreferencesState, usePreferencesActions } from '../contexts/preferences/PreferencesProvider';
export function SettingsScreen() {
const { theme, language, isHydrated } = usePreferencesState();
const { toggleTheme, setLanguage } = usePreferencesActions();
if (!isHydrated) return <Text>Carregando...</Text>;
return (
<View>
<Text>Tema atual: {theme}</Text>
<Button title="Alternar tema" onPress={toggleTheme} />
<Text>Idioma: {language}</Text>
<Button title="Português" onPress={() => setLanguage('pt-BR')} />
<Button title="English" onPress={() => setLanguage('en-US')} />
</View>
);
}Lista de produtos (adiciona ao carrinho) + badge com total derivado
// src/components/CartBadge.tsx
import React from 'react';
import { Text } from 'react-native';
import { useCartSummary } from '../contexts/cart/CartProvider';
export function CartBadge() {
const { totalItems } = useCartSummary();
return <Text>Carrinho: {totalItems}</Text>;
}// src/screens/ProductsScreen.tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCartActions } from '../contexts/cart/CartProvider';
const PRODUCTS = [
{ id: 'p1', title: 'Camiseta', price: 59.9 },
{ id: 'p2', title: 'Caneca', price: 29.9 },
];
export function ProductsScreen() {
const { addItem } = useCartActions();
return (
<View>
{PRODUCTS.map((p) => (
<View key={p.id}>
<Text>{p.title} - R$ {p.price.toFixed(2)}</Text>
<Button title="Adicionar" onPress={() => addItem(p)} />
</View>
))}
</View>
);
}Tela do carrinho (estado + ações + total)
// src/screens/CartScreen.tsx
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCartState, useCartActions, useCartSummary } from '../contexts/cart/CartProvider';
export function CartScreen() {
const { items, isHydrated } = useCartState();
const { setQty, removeItem, clear } = useCartActions();
const { totalPrice } = useCartSummary();
if (!isHydrated) return <Text>Carregando...</Text>;
return (
<View>
{items.length === 0 ? <Text>Carrinho vazio</Text> : null}
{items.map((i) => (
<View key={i.id}>
<Text>{i.title} (x{i.quantity}) - R$ {(i.price * i.quantity).toFixed(2)}</Text>
<Button title="+" onPress={() => setQty(i.id, i.quantity + 1)} />
<Button title="-" onPress={() => setQty(i.id, i.quantity - 1)} />
<Button title="Remover" onPress={() => removeItem(i.id)} />
</View>
))}
<Text>Total: R$ {totalPrice.toFixed(2)}</Text>
<Button title="Limpar carrinho" onPress={clear} />
</View>
);
}Reidratação coordenada: evitando “flash” de estado padrão
Quando você persiste estado, é comum o app iniciar com valores padrão e, alguns milissegundos depois, trocar para o estado reidratado. Para evitar esse “flash”, use flags como isHydrated (como fizemos) e só renderize a UI principal quando os domínios críticos estiverem prontos.
Exemplo de gate simples:
// src/Root.tsx
import React from 'react';
import { Text } from 'react-native';
import { useAuthState } from './contexts/auth/AuthProvider';
import { usePreferencesState } from './contexts/preferences/PreferencesProvider';
import { useCartState } from './contexts/cart/CartProvider';
export function Root() {
const auth = useAuthState();
const prefs = usePreferencesState();
const cart = useCartState();
const ready = auth.isHydrated && prefs.isHydrated && cart.isHydrated;
if (!ready) return <Text>Inicializando...</Text>;
return <Text>App pronto</Text>;
}Alertas de performance: por que separar estado e ações ajuda
- Se um componente consome apenas ações (ex.:
signOut), ele não precisa re-renderizar quandousermuda. - Se você coloca
{state, actions}no mesmo Context, qualquer mudança emstatemuda o objeto inteiro e re-renderiza todos os consumidores. - Mesmo com separação, componentes que consomem o estado ainda re-renderizam quando ele muda. Para estados muito “quentes” (mudam com alta frequência), Context pode virar gargalo.
Dica: estabilize referências com useMemo/useCallback
Se você recria funções/objetos a cada render do Provider, o Context de ações também muda e causa renders. Por isso usamos useCallback nas funções e useMemo para o objeto actions.
Quando Context não é suficiente (sinais de complexidade)
Context API é ótima, mas tem limites. Considere alternativas (ou uma arquitetura mais robusta) quando aparecerem sinais como:
- Muitos contexts interdependentes com “efeitos em cascata” (ex.: atualizar Auth precisa limpar Cart, atualizar Preferences precisa recalcular várias telas).
- Atualizações muito frequentes (ex.: tracking em tempo real, digitação sincronizada, streaming de eventos) causando re-renderizações amplas.
- Estado muito normalizado e complexo (entidades relacionadas, cache de requests, invalidação, paginação, deduplicação).
- Debug difícil: você precisa rastrear “quem alterou o quê” e sente falta de ferramentas de inspeção/time-travel.
- Regras de negócio espalhadas: ações começam a chamar outras ações, reducers crescem demais e você cria muitos “helpers” para manter consistência.
Mitigações antes de trocar de abordagem
- Dividir por domínio (como fizemos) e evitar um Context único.
- Separar estado e ações e manter ações estáveis.
- Extrair seletores (funções que calculam derivados) e memorizar com
useMemo. - Evitar colocar no Context dados que podem ficar locais (ex.: estado de formulário de uma tela).
- Reduzir o tamanho do estado persistido: persista apenas o necessário (ex.: token, preferências, itens do carrinho), não caches enormes.
Checklist de implementação (passo a passo)
| Passo | O que fazer | Resultado |
|---|---|---|
| 1 | Definir tipos e estado inicial | Contrato claro do domínio |
| 2 | Criar reducer com ações explícitas | Atualizações previsíveis |
| 3 | Criar storage (load/save/clear) | Persistência e reidratação |
| 4 | Provider com useReducer + useEffect (hydrate/persist) | Estado global funcional |
| 5 | Separar Context de estado e de ações | Menos renders em consumidores de ações |
| 6 | Expor hooks (useXState, useXActions, seletores) | Consumo simples e padronizado |
| 7 | Adicionar gate de isHydrated no root | Sem “flash” de estado padrão |