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
- Documentação oficial do React: Context — Guia completo sobre createContext, Provider e Consumer
- Documentação oficial do React: useContext — Referência completa do hook useContext com exemplos
- React Context API: Um guia prático (Rocketseat) — Tutorial prático em português sobre Context API
- How to use React Context effectively (Kent C. Dodds) — Artigo técnico sobre boas práticas com Context API
- useReducer vs useState: quando usar cada um (Dev.to) — Comparação detalhada entre useState e useReducer no React
- Zustand: estado global minimalista — Repositório oficial da biblioteca Zustand, alternativa ao Context para estado global
- Jotai: gerenciamento de estado atômico — Documentação oficial do Jotai, abordagem atômica para estado global em React