Tipar useContext no React com TypeScript
Context API sem tipagem e receita pra dor de cabeca. Aprenda a tipar createContext, Provider e useContext do jeito que projetos profissionais fazem.
Por que isso é importante
Tipar useContext no React com TypeScript. Context API sem tipagem e receita pra dor de cabeca. Aprenda a tipar createContext, Provider e useContext do jeito que projetos profissionais fazem.
Como o TypeScript funciona com createContext
O createContext do React aceita um generico que define o formato do valor que o contexto vai carregar. O ponto que confunde a galera e o valor default: voce precisa passar um valor inicial que bata com o tipo declarado. Se nao quiser passar um valor real, pode usar null ou undefined, mas ai precisa tratar isso no consumo.
Existem duas abordagens principais. A primeira e passar o valor default completo no createContext. A segunda, mais comum em projetos reais, e passar null como default e criar um hook customizado que faz o type guard pra voce. Vamos ver as duas.
import { createContext } from 'react';
// Abordagem 1: valor default completo
interface ThemeContext {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContext>({
theme: 'light',
toggleTheme: () => {}, // funcao noop como default
});
// Abordagem 2: null como default (mais segura)
const ThemeContext2 = createContext<ThemeContext | null>(null);
Passo a passo: Context tipado do zero
Esse fluxo garante que qualquer componente que tente usar o contexto fora do Provider receba um erro claro em vez de valores undefined silenciosos.
Exemplos praticos com Context tipado
Auth Context completo com TypeScript
O caso mais classico de Context: autenticacao. Voce quer compartilhar o usuario logado, funcoes de login/logout e estado de loading por toda a aplicacao. Veja como fica com tipagem de ponta a ponta:
import {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from 'react';
// 1. Interface do contexto
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
// 2. createContext com null default
const AuthContext = createContext<AuthContextType | null>(null);
// 3. Hook customizado com type guard
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth precisa estar dentro de AuthProvider');
}
return context;
}
// 4. Provider tipado
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = useCallback(
async (email: string, password: string) => {
setIsLoading(true);
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data: User = await res.json();
setUser(data);
} finally {
setIsLoading(false);
}
},
[]
);
const logout = useCallback(() => {
setUser(null);
}, []);
const value: AuthContextType = {
user,
isAuthenticated: !!user,
isLoading,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
Consumindo o contexto nos componentes
Agora que o contexto ta tipado, o autocomplete do seu editor faz o trabalho pesado. Voce digita user. e ja ve id, name, email, role. Sem any, sem casting, sem chute.
// Componente que consome o AuthContext
function UserGreeting() {
const { user, isAuthenticated, logout } = useAuth();
// TypeScript sabe exatamente o que cada campo contem
if (!isAuthenticated) {
return <p>Faca login para continuar</p>;
}
return (
<div>
<p>Ola, {user?.name}</p>
<p>Role: {user?.role}</p>
<button onClick={logout}>Sair</button>
</div>
);
}
// Uso na arvore de componentes
function App() {
return (
<AuthProvider>
<UserGreeting />
</AuthProvider>
);
}
Context com reducer tipado
Pra estados mais complexos, combinar Context com useReducer e o caminho. O TypeScript brilha aqui porque voce tipa as actions com discriminated unions e o compilador garante que cada case do reducer trata os dados certos.
import { createContext, useContext, useReducer, ReactNode } from 'react';
// Estado tipado
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
// Actions com discriminated union
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: { id: string } }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' };
// Contexto com state + dispatch
interface CartContextType {
state: CartState;
dispatch: React.Dispatch<CartAction>;
}
const CartContext = createContext<CartContextType | null>(null);
export function useCart(): CartContextType {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart fora do CartProvider');
return ctx;
}
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const exists = state.items.find(i => i.id === action.payload.id);
if (exists) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(i => i.id !== action.payload.id),
};
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}
Erros comuns ao tipar Context
Armadilhas frequentes
O erro mais frequente e criar o Context sem generico e acabar com Context<undefined>. Outro classico: usar useContext direto sem verificar null, o que faz o TypeScript reclamar toda vez que voce tenta acessar uma propriedade. Sempre crie um hook customizado com o throw — ele resolve os dois problemas de uma vez.
// ERRADO: Context sem generico
const MyContext = createContext(undefined);
// Tipo inferido: Context<undefined> — inutil
// ERRADO: useContext direto sem null check
function Component() {
const ctx = useContext(MyContext);
// ctx pode ser null, TypeScript reclama:
console.log(ctx.user); // Error: Object is possibly 'null'
}
// ERRADO: valor default com tipo incompleto
const BadContext = createContext<AuthContextType>({
user: null,
isAuthenticated: false,
// Faltou login e logout!
// TypeScript Error: Property 'login' is missing
});
// CERTO: null default + hook com type guard
const GoodContext = createContext<AuthContextType | null>(null);
function useGoodContext() {
const ctx = useContext(GoodContext);
if (!ctx) throw new Error('Fora do Provider');
return ctx; // Tipo: AuthContextType (sem null)
}
Checklist: Context tipado profissional
Checklist Final
Evolua sua tipagem React com projeto real
Projeto completo com TypeScript
Tipar Context e uma habilidade que separa quem coda React de quem domina React. No CrazyStack voce constroi um projeto real inteiro usando Context API, hooks customizados e TypeScript do inicio ao deploy. Tudo com as praticas que empresas grandes usam.