Tipar Context Provider no React + TS 2026
Context sem tipagem vira terra de ninguém. Aprenda o padrão completo: createContext + Provider + hook customizado, tudo tipado com TypeScript.
Por que isso é importante
Tipar Context Provider no React + TS 2026. Context sem tipagem vira terra de ninguém. Aprenda o padrão completo: createContext + Provider + hook customizado, tudo tipado com TypeScript.
O padrão completo: createContext + Provider + hook
Galera, o segredo de um context bem tipado é seguir um padrão de 3 partes. Primeiro, o createContext com o tipo certo. Segundo, o Provider que encapsula a lógica. Terceiro, um hook customizado que consome o context e já garante que o valor não é undefined.
Esse padrão elimina o problema clássico de fazer useContext e receber undefined porque esqueceu de envolver com Provider. O hook customizado trata isso e dispara erro claro.
Passo a passo: context tipado do zero
Exemplo completo: AuthContext tipado
Vamos criar um contexto de autenticação completo: estado do user, funções de login/logout, loading. Tudo tipado.
import {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
// 1. Interface do usuário
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// 2. Interface do contexto
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// 3. Criar context com undefined (sem valor fake)
const AuthContext = createContext<AuthContextType | undefined>(
undefined
);
// 4. Provider
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const login = useCallback(
async (email: string, password: string) => {
setLoading(true);
try {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const data: User = await res.json();
setUser(data);
} finally {
setLoading(false);
}
},
[]
);
const logout = useCallback(() => {
setUser(null);
}, []);
return (
<AuthContext.Provider
value={{
user,
loading,
login,
logout,
isAuthenticated: user !== null,
}}
>
{children}
</AuthContext.Provider>
);
}
// 5. Hook customizado (a estrela do padrão)
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth deve ser usado dentro de AuthProvider");
}
return context;
}
Quem chama useAuth() recebe user, login, logout, loading e isAuthenticated — tudo tipado. Se esquecer de envolver com AuthProvider, o erro aparece na hora, claro e direto.
Context com useReducer: dispatch tipado
Pra contextos mais complexos, useReducer é melhor que useState. E com TypeScript, dá pra tipar cada action com discriminated union — o dispatch só aceita actions válidas.
// State tipado
interface CartState {
items: CartItem[];
total: number;
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
// Actions com discriminated union
type CartAction =
| { type: "ADD_ITEM"; payload: CartItem }
| { type: "REMOVE_ITEM"; payload: { id: string } }
| { type: "UPDATE_QUANTITY"; payload: { id: string; quantity: number } }
| { type: "CLEAR_CART" };
// Reducer tipado
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case "ADD_ITEM":
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
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;
}
}
// Context com dispatch tipado
interface CartContextType {
state: CartState;
dispatch: React.Dispatch<CartAction>;
}
const CartContext = createContext<CartContextType | undefined>(
undefined
);
export function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0,
});
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
export function useCart(): CartContextType {
const context = useContext(CartContext);
if (!context) throw new Error("useCart fora do CartProvider");
return context;
}
// Uso:
const { state, dispatch } = useCart();
dispatch({ type: "ADD_ITEM", payload: item });
// TypeScript garante que payload casa com o type
Se alguém tentar dispatch({ type: 'ADD_ITEM' }) sem o payload, o TypeScript barra na hora. Cada action tem a estrutura correta garantida em compile-time.
Context genérico reutilizável
Dá pra criar uma factory de context que funciona com qualquer tipo. Isso evita repetir o boilerplate de createContext + Provider + hook em todo contexto novo.
function createSafeContext<T>(name: string) {
const Context = createContext<T | undefined>(undefined);
function useContextSafe(): T {
const value = useContext(Context);
if (value === undefined) {
throw new Error(
`use${name} deve ser usado dentro de ${name}Provider`
);
}
return value;
}
return [Context.Provider, useContextSafe] as const;
}
// Uso:
const [ThemeProvider, useTheme] =
createSafeContext<ThemeContextType>("Theme");
const [NotificationProvider, useNotification] =
createSafeContext<NotificationContextType>("Notification");
Com essa factory, criar um novo context tipado leva uma linha. O boilerplate de verificação e error handling fica encapsulado.
Erros comuns ao tipar Context
Passar valor default fake no createContext: createContext({} as AuthContextType) esconde o erro de Provider ausente. Use undefined + validação no hook.
Não criar hook customizado: usar useContext(AuthContext) direto espalha a verificação de undefined pelo app inteiro.
Expor o context diretamente: só exporte o Provider e o hook. O objeto de context fica privado no arquivo.
Não tipar actions do reducer: dispatch sem tipo aceita qualquer objeto, e bugs aparecem só em runtime.
Colocar lógica pesada no Provider: chamadas de API e cálculos complexos dentro do Provider re-renderizam todos os consumers. Use useMemo e useCallback.
Checklist: Context Provider tipado
Checklist Final
Context profissional: aplique em projeto real
Context tipado é peça-chave em qualquer app React profissional. No CrazyStack, você implementa contextos reais — Auth, Cart, Theme, Notification — todos com TypeScript, dentro de um projeto completo com Node.js e React.