Stash da Ana
reactcontext-apitypescripttutorial

Context API no React: compartilhando estado sem prop drilling

3 min de leitura

Prop drilling é o nome do desconforto que aparece quando um valor mora num componente alto na árvore, é usado por um componente baixo, e precisa atravessar todo mundo no meio só para chegar lá. O estado do tema do site é um exemplo típico: ele costuma viver no layout raiz, mas o componente que alterna o tema é uma folha qualquer dentro do header.

App (tem o estado `theme`)
  └── Layout (recebe theme, repassa)
        └── Header (recebe theme, repassa)
              └── ThemeToggle (finalmente usa theme!)

Cada nível intermediário precisa carregar uma prop que não usa, e qualquer mudança na assinatura — renomear theme para colorScheme, adicionar um campo — propaga pela cadeia inteira. É o tipo de fricção que parece bobagem em um exemplo de quatro componentes e vira inferno em uma aplicação real.

A Context API

A Context API é o mecanismo do próprio React para furar essa árvore. Um valor publicado num Provider fica disponível para qualquer descendente, sem passar por ninguém no meio — independente de quantos níveis existam entre o Provider e o consumidor.

Criando o contexto

import { createContext } from 'react'
 
type Theme = 'light' | 'dark'
 
interface ThemeContextValue {
  theme: Theme
  toggleTheme: () => void
}
 
const ThemeContext = createContext<ThemeContextValue | null>(null)

createContext recebe o valor padrão. Usar null aqui em vez de um objeto vazio é proposital: o contexto só faz sentido dentro do ThemeProvider, e o tipo ThemeContextValue | null força quem consome a tratar o caso de "não tem Provider acima".

O Provider

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light')
 
  const toggleTheme = () => {
    const next: Theme = theme === 'dark' ? 'light' : 'dark'
    setTheme(next)
    localStorage.setItem('theme', next)
    document.documentElement.classList.toggle('dark', next === 'dark')
  }
 
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

O Provider é um componente normal: segura o estado, expõe o que quiser pelo value, e qualquer componente abaixo na árvore passa a ter acesso direto a esse objeto.

O hook de consumo

'use client'
 
import { useContext } from 'react'
 
export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
  return ctx
}

O if (!ctx) substitui um crash silencioso ("cannot read property theme of null") por uma mensagem que aponta direto para o erro real — alguém esqueceu de envolver o componente com o Provider. Esse pequeno hook costuma valer mais que o useContext cru exatamente por causa dessa linha.

O componente final

'use client'
 
import { useTheme } from './ThemeProvider'
 
export function ThemeToggle() {
  const { theme, toggleTheme } = useTheme()
 
  return (
    <button onClick={toggleTheme}>
      {theme === 'dark' ? 'Claro' : 'Escuro'}
    </button>
  )
}

O ThemeToggle não tem props relacionadas ao tema. Não importa quantos níveis existam entre ele e o Provider — useTheme resolve.

Onde o Provider é registrado

Com App Router, o Provider mora no layout.tsx raiz, envolvendo tudo:

import { ThemeProvider } from '@/components/ThemeProvider'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

A partir daqui, qualquer componente cliente abaixo dessa árvore pode consumir o contexto. Vale lembrar que useContext só faz sentido em Client Components; é por isso que tanto o ThemeProvider quanto o useTheme levam 'use client' no topo.

Quando usar Context (e quando não usar)

Context se encaixa bem em estado global e de baixa frequência de atualização: tema, idioma, usuário autenticado, configuração de interface. Para estado local de um formulário ou de uma lista, useState no próprio componente continua sendo o caminho.

A objeção mais comum é que toda mudança no value re-renderiza todos os consumidores. É verdade — e o problema é tipicamente menor do que blogs e tutoriais sugerem. Para tema, que muda umas duas vezes por sessão, o custo é nulo. O cuidado vale quando o value muda dezenas de vezes por segundo (posição de drag, scroll, animação): aí Context atrapalha mesmo, e ferramentas como Zustand, Jotai ou Redux passam a fazer sentido.