useContext: gerenciamento de estado global simples

1. Introdução ao Context API e useContext

Em aplicações React, um dos problemas mais comuns é o prop drilling — a prática de passar props através de múltiplos níveis de componentes apenas para que um componente filho distante possa acessar aquele dado. Isso torna o código verboso, difícil de manter e propenso a erros.

O Context API foi criado para resolver exatamente esse problema. Ele permite compartilhar dados entre componentes sem precisar passá-los manualmente por cada nível da árvore. O Context API funciona com dois elementos principais:

  • Provider: componente que fornece os dados para todos os componentes descendentes
  • Consumer: forma de consumir os dados do contexto

O hook useContext veio para simplificar ainda mais esse consumo, eliminando a necessidade de usar o padrão render props com Consumer.

2. Criando e configurando um Contexto

Criar um contexto é simples. Usamos React.createContext() e definimos um valor padrão:

import { createContext } from 'react';

// Valor padrão: tema claro
export const ThemeContext = createContext('light');

Um contexto possui três partes:
- Provider: componente que "entrega" os dados
- Consumer: componente que consome os dados (forma antiga)
- Valor padrão: usado quando um componente consome o contexto sem um Provider acima

Vamos criar um contexto de tema funcional:

import { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

3. O Provider: fornecendo dados para a árvore de componentes

O Provider envolve os componentes que precisam acessar os dados. Ele aceita uma prop value que contém os dados a serem compartilhados.

import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';

function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

Boas práticas com providers aninhados:

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <I18nProvider>
          <MainApp />
        </I18nProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Cada provider gerencia seu próprio escopo de dados, evitando contextos gigantescos.

4. Consumindo contexto com useContext

O hook useContext é a forma mais limpa de consumir um contexto:

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <h1>Meu App</h1>
      <button onClick={toggleTheme}>
        Mudar para {theme === 'light' ? 'escuro' : 'claro'}
      </button>
    </header>
  );
}

Diferença entre Consumer e useContext:

Consumer (render props):

<ThemeContext.Consumer>
  {({ theme }) => <div>Tema atual: {theme}</div>}
</ThemeContext.Consumer>

useContext (hook):

const { theme } = useContext(ThemeContext);
return <div>Tema atual: {theme}</div>;

O hook é mais legível e evita aninhamento desnecessário.

5. Estado global com useContext + useState/useReducer

Combinando com useState para estado simples:

import { createContext, useState, useContext } from 'react';

const UserContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

// Consumindo
function Profile() {
  const { user, logout } = useContext(UserContext);
  if (!user) return <p>Deslogado</p>;
  return (
    <div>
      <p>Bem-vindo, {user.name}!</p>
      <button onClick={logout}>Sair</button>
    </div>
  );
}

Usando useReducer para lógica complexa:

import { createContext, useContext, useReducer } from 'react';

const CartContext = createContext();

const initialState = { items: [], total: 0 };

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      };
    case 'REMOVE_ITEM':
      const filtered = state.items.filter(item => item.id !== action.payload.id);
      return {
        ...state,
        items: filtered,
        total: state.total - action.payload.price
      };
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

// Componente usando o carrinho
function CartItem({ item }) {
  const { dispatch } = useContext(CartContext);

  return (
    <div>
      <span>{item.name} - R${item.price}</span>
      <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item })}>
        Remover
      </button>
    </div>
  );
}

6. Performance e boas práticas com useContext

Evitando re-renderizações desnecessárias:

import React, { memo, useContext } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent() {
  const { user } = useContext(UserContext);
  return <div>{user?.name}</div>;
});

Separando contextos por responsabilidade:

// AuthContext.js — apenas dados de autenticação
// ThemeContext.js — apenas tema
// CartContext.js — apenas carrinho

Quando usar useContext vs outras soluções:

Situação Solução recomendada
Estado simples (tema, idioma) useContext
Estado moderado (auth, carrinho) useContext + useReducer
Estado complexo e global Redux, Zustand
Performance crítica Jotai, Recoil

7. Casos de uso comuns e exemplos práticos

Autenticação de usuário:

function LoginButton() {
  const { login } = useContext(AuthContext);
  const handleLogin = () => {
    login({ id: 1, name: 'João', token: 'abc123' });
  };
  return <button onClick={handleLogin}>Entrar</button>;
}

Preferências de idioma:

function Greeting() {
  const { lang } = useContext(I18nContext);
  const messages = { pt: 'Olá', en: 'Hello', es: 'Hola' };
  return <h1>{messages[lang]}</h1>;
}

8. Limitações e alternativas ao useContext

Problemas comuns:
- Re-renderizações em cascata: qualquer mudança no contexto força todos os consumidores a re-renderizar
- Contextos grandes: dificultam a performance e a manutenção
- Dificuldade de testar: contextos aninhados podem complicar testes unitários

Alternativas modernas:

  • Zustand: biblioteca mínima e performática para estado global
  • Jotai: abordagem atômica, re-renderiza apenas componentes que consomem o átomo modificado
  • Recoil: gerenciamento de estado global do Facebook, com suporte a async

Para aplicações pequenas/médias, useContext combinado com useReducer é mais que suficiente. Para projetos maiores, avalie as alternativas conforme a complexidade.

Referências