<h2>Introdução: Por que Context API com TypeScript?</h2>
<p>A Context API é uma ferramenta nativa do React que permite compartilhar dados entre componentes sem a necessidade de prop drilling — aquele problema comum quando você precisa passar props através de vários níveis de componentes. Quando combinada com TypeScript, a Context API se torna ainda mais poderosa, oferecendo tipagem robusta e autocompletar inteligente, reduzindo erros em tempo de desenvolvimento e facilitando a manutenção do código.</p>
<p>Muitos desenvolvedores evitam TypeScript com Context API por acreditar que é complexo. A verdade é que, com as práticas corretas, é incrivelmente simples e oferece uma experiência de desenvolvimento superior. Este artigo vai te guiar através de padrões profissionais que você pode usar em produção imediatamente.</p>
<h2>Entendendo os Fundamentos da Context API Tipada</h2>
<h3>O que é Context e por que tipagem importa</h3>
<p>A Context API funciona criando um contexto que armazena dados e os disponibiliza para qualquer componente descendente. Sem TypeScript, você pode facilmente passar tipos errados ou acessar propriedades que não existem. Com TypeScript, o compilador avisa você antes do código rodar — economizando horas de debug.</p>
<p>A tipagem em Context API envolve tipificar três coisas: o estado do contexto, o valor fornecido pelo provider e os hooks customizados que consomem esse contexto. Quando feito corretamente, você obtém autocomplete perfeito e segurança de tipos em todo seu código.</p>
<h3>Estrutura básica de um contexto tipado</h3>
<p>Vamos criar um exemplo real: um contexto de autenticação que armazena dados do usuário.</p>
<pre><code class="language-typescript">// AuthContext.tsx
import React, { createContext, useState, ReactNode, useContext } from 'react';
// Definir o tipo do usuário
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// Definir o tipo do valor do contexto
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// Criar o contexto com valor inicial undefined
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Provider component
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string): Promise<void> => {
setIsLoading(true);
try {
// Simulando chamada à API
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setUser(data.user);
} finally {
setIsLoading(false);
}
};
const logout = (): void => {
setUser(null);
};
const value: AuthContextType = {
user,
isLoading,
login,
logout,
isAuthenticated: user !== null,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// Hook customizado para usar o contexto
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth deve ser usado dentro de AuthProvider');
}
return context;
};</code></pre>
<p>Note que criamos o contexto com <code>undefined</code> como tipo padrão. Isso nos permite capturar erros em tempo de desenvolvimento se alguém tentar usar o hook fora do provider. O hook <code>useAuth</code> garante que o contexto está sempre disponível quando usado corretamente.</p>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Separação de responsabilidades: Context e Reducer</h3>
<p>Para contextos mais complexos, separar a lógica do estado usando <code>useReducer</code> torna o código mais testável e escalável. Aqui está um exemplo de um contexto de carrinho de compras:</p>
<pre><code class="language-typescript">// CartContext.tsx
import React, {
createContext,
useReducer,
ReactNode,
useContext,
Dispatch
} from 'react';
interface Product {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: Product[];
total: number;
}
type CartAction =
{ type: 'ADD_ITEM'; payload: Product } | { type: 'REMOVE_ITEM'; payload: string } | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } } | { type: 'CLEAR_CART' };
interface CartContextType {
state: CartState;
dispatch: Dispatch<CartAction>;
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
clearCart: () => void;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload.id);
const newItems = existingItem
? state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
: [...state.items, action.payload];
const newTotal = newItems.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
return { items: newItems, total: newTotal };
}
case 'REMOVE_ITEM':
const filteredItems = state.items.filter(item => item.id !== action.payload);
const removedTotal = filteredItems.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
return { items: filteredItems, total: removedTotal };
case 'CLEAR_CART':
return { items: [], total: 0 };
case 'UPDATE_QUANTITY': {
const updatedItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
const updatedTotal = updatedItems.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
return { items: updatedItems, total: updatedTotal };
}
default:
return state;
}
};
interface CartProviderProps {
children: ReactNode;
}
export const CartProvider: React.FC<CartProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0,
});
const addItem = (product: Product): void => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (productId: string): void => {
dispatch({ type: 'REMOVE_ITEM', payload: productId });
};
const clearCart = (): void => {
dispatch({ type: 'CLEAR_CART' });
};
const value: CartContextType = {
state,
dispatch,
addItem,
removeItem,
clearCart,
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
};
export const useCart = (): CartContextType => {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error('useCart deve ser usado dentro de CartProvider');
}
return context;
};</code></pre>
<p>Este padrão é poderoso porque a lógica do reducer é pura — fácil de testar e debugar. TypeScript garante que cada ação tenha a estrutura correta.</p>
<h3>Múltiplos Contextos e Composição</h3>
<p>Em aplicações reais, você frequentemente precisa de vários contextos. Em vez de aninhar vários providers, crie um componente que los combine:</p>
<pre><code class="language-typescript">// RootProvider.tsx
import React, { ReactNode } from 'react';
import { AuthProvider } from './AuthContext';
import { CartProvider } from './CartContext';
import { ThemeProvider } from './ThemeContext';
interface RootProviderProps {
children: ReactNode;
}
export const RootProvider: React.FC<RootProviderProps> = ({ children }) => {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
};
// E no App.tsx
import { RootProvider } from './providers/RootProvider';
function App() {
return (
<RootProvider>
<YourApp />
</RootProvider>
);
}</code></pre>
<p>Dessa forma, você evita "provider hell" e mantém seu código organizado e legível.</p>
<h3>Consumindo múltiplos contextos em um componente</h3>
<p>Às vezes você precisa usar dados de vários contextos no mesmo componente. TypeScript ajuda a garantir que você está acessando tudo corretamente:</p>
<pre><code class="language-typescript">// CheckoutComponent.tsx
import React from 'react';
import { useAuth } from './AuthContext';
import { useCart } from './CartContext';
import { useTheme } from './ThemeContext';
const CheckoutComponent: React.FC = () => {
const { user, isAuthenticated } = useAuth();
const { state: cartState, clearCart } = useCart();
const { theme } = useTheme();
const handleCheckout = async (): Promise<void> => {
if (!isAuthenticated || !user) {
console.error('Usuário não autenticado');
return;
}
try {
const response = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
userId: user.id,
items: cartState.items,
total: cartState.total,
}),
});
if (response.ok) {
clearCart();
}
} catch (error) {
console.error('Erro no checkout:', error);
}
};
return (
<div style={{ background: theme === 'dark' ? '#000' : '#fff' }}>
<h2>Checkout para {user?.name}</h2>
<p>Total: R$ {cartState.total.toFixed(2)}</p>
<button onClick={handleCheckout}>Finalizar Pedido</button>
</div>
);
};
export default CheckoutComponent;</code></pre>
<h2>Otimização de Performance e Selectors</h2>
<h3>Evitando re-renders desnecessários</h3>
<p>Um problema comum com Context API é que qualquer mudança no valor do contexto causa re-render em todos os componentes que o consomem. Para evitar isso, você pode criar selectors:</p>
<pre><code class="language-typescript">// useAuthSelector.ts
import { useAuth } from './AuthContext';
// Selector para pegar apenas o usuário
export const useAuthUser = () => {
const { user } = useAuth();
return user;
};
// Selector para pegar apenas o status de autenticação
export const useIsAuthenticated = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated;
};
// Selector para pegar apenas o status de carregamento
export const useAuthLoading = () => {
const { isLoading } = useAuth();
return isLoading;
};
// Componente que usa apenas o que precisa
const UserProfile: React.FC = () => {
const user = useAuthUser(); // Só re-renderiza se user mudar
if (!user) return <div>Usuário não carregado</div>;
return <div>{user.name}</div>;
};</code></pre>
<p>Uma abordagem mais robusta é usar <code>useMemo</code> dentro do provider para evitar que o valor do contexto mude em cada render:</p>
<pre><code class="language-typescript">// AuthContext.tsx (versão melhorada)
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
// ... funções login e logout ...
// Memoizar o valor para evitar re-renders
const value = React.useMemo(
() => ({
user,
isLoading,
login,
logout,
isAuthenticated: user !== null,
}),
[user, isLoading]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};</code></pre>
<h3>Dividindo contextos para melhor performance</h3>
<p>Para contextos grandes, dividir em múltiplos contextos menores é uma estratégia eficaz:</p>
<pre><code class="language-typescript">// AuthStateContext.ts - apenas dados
interface AuthState {
user: User | null;
isLoading: boolean;
}
const AuthStateContext = createContext<AuthState | undefined>(undefined);
// AuthActionsContext.ts - apenas funções
interface AuthActions {
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthActionsContext = createContext<AuthActions | undefined>(undefined);
// Agora componentes que só precisam ler dados não re-renderizam
// quando uma ação é executada
export const useAuthState = () => {
const context = useContext(AuthStateContext);
if (!context) throw new Error('useAuthState deve estar dentro de AuthProvider');
return context;
};
export const useAuthActions = () => {
const context = useContext(AuthActionsContext);
if (!context) throw new Error('useAuthActions deve estar dentro de AuthProvider');
return context;
};</code></pre>
<h2>Tratamento de Erros e Validação</h2>
<h3>Tipagem segura com optional chaining</h3>
<p>TypeScript nos permite ser explícitos sobre possíveis valores nulos:</p>
<pre><code class="language-typescript"></code></pre>
<h3>Tratamento de erros no contexto</h3>
<p>Adicionar erro como parte do estado é essencial para aplicações robustas:</p>
<pre><code class="language-typescript">// NotificationContext.tsx
interface Notification {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
interface NotificationContextType {
notifications: Notification[];
addNotification: (message: string, type: Notification['type']) => void;
removeNotification: (id: string) => void;
}
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = (message: string, type: Notification['type']): void => {
const id = Date.now().toString();
const notification: Notification = { id, message, type };
setNotifications(prev => [...prev, notification]);
// Auto-remover depois de 5 segundos
if (notification.duration !== 0) {
setTimeout(() => removeNotification(id), notification.duration || 5000);
}
};
const removeNotification = (id: string): void => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
{children}
</NotificationContext.Provider>
);
};
export const useNotification = (): NotificationContextType => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification deve estar dentro de NotificationProvider');
}
return context;
};
// Usando com tratamento de erro
const MyComponent: React.FC = () => {
const { addNotification } = useNotification();
const { login } = useAuth();
const handleLogin = async (email: string, password: string): Promise<void> => {
try {
await login(email, password);
addNotification('Login realizado com sucesso!', 'success');
} catch (error) {
const message = error instanceof Error ? error.message : 'Erro ao fazer login';
addNotification(message, 'error');
}
};
return <button onClick={() => handleLogin('user@example.com', 'pass')}>Login</button>;
};</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu três conceitos fundamentais que transformam a forma como trabalha com estado global em React com TypeScript. Primeiro, a importância de tipagem explícita em contextos — criar interfaces claras para seus dados e ações evita bugs e oferece autocomplete perfeito. Segundo, padrões arquiteturais avançados como separação de state e actions com <code>useReducer</code>, além de composição de múltiplos contextos, tornam seu código escalável e testável. Terceiro, otimização através de selectors e divisão de contextos é essencial para evitar re-renders desnecessários conforme sua aplicação cresce.</p>
<p>Com essas práticas em mãos, você está pronto para construir sistemas robustos de gerenciamento de estado em React que são fáceis de manter, debugar e estender.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Context API - Documentação Oficial</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Generics</a></li>
<li><a href="https://blog.logrocket.com/advanced-react-patterns/" target="_blank" rel="noopener noreferrer">Advanced Patterns with React Context - LogRocket Blog</a></li>
<li><a href="https://kentcdodds.com/blog/how-to-use-react-context-effectively" target="_blank" rel="noopener noreferrer">React useReducer Hook with TypeScript - Kent C. Dodds</a></li>
</ul>
<p><!-- FIM --></p>