Pular para o conteúdo
TypeScript

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

  1. Crie a interface que define o formato do seu contexto
  2. Use createContext com o generico da interface
  3. Crie o Provider como componente que recebe children e gerencia o estado
  4. Exporte um hook customizado que consome o contexto com type guard
  5. Envolva sua arvore de componentes com o Provider
  6. Consuma o contexto usando o hook customizado, nunca useContext direto

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
    </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. 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

  • Interface do contexto define todos os campos e funcoes
  • createContext usa generico com | null para default seguro
  • Hook customizado exportado com throw para uso fora do Provider
  • Provider recebe children tipado como ReactNode
  • Funcoes do contexto estabilizadas com useCallback
  • Actions de reducer usam discriminated unions
  • Nenhum useContext usado diretamente nos componentes
  • Contextos separados por dominio (auth, theme, cart)

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.