Context API no React: estado sem ficar passando props
Como criar contexto, envolver a árvore com o provider e ler com useContext — e por que todo componente abaixo re-renderiza quando o valor muda.
Imagine a recepção de uma clínica lotada. O botão que troca o tema da tela está
no fundo da aplicação, mas cada componente no caminho precisa receber tema e
entregar tema ao próximo — mesmo sem usar essa informação. O código funciona,
mas uma mudança simples vira uma fila de repasses.
A Context API resolve justamente esse transporte. Ao final deste artigo, você
vai saber criar um contexto, escolher onde colocar o provider, ler o valor com
useContext, reconhecer renders desnecessários e decidir quando contexto não é
a ferramenta certa.
Pense no sistema de avisos de um prédio. createContext abre um canal; o
provider — componente que fornece o valor — liga o alto-falante nos andares
que estão abaixo dele; e useContext funciona como o aparelho que escuta aquele
canal. Os componentes no corredor deixam de repetir a mensagem de porta em
porta.
A analogia tem um limite importante: o alto-falante real pode guardar uma
gravação, mas o contexto não guarda estado. Ele apenas transporta o value que
recebe. Também não é global automaticamente: somente descendentes do provider
ouvem o canal, e o React compara o valor anterior com o novo usando Object.is,
uma comparação nativa do JavaScript, para atualizar os consumidores.
Todos os exemplos pertencem ao painel da clínica veterinária Pata Amiga: tema
claro e escuro, fila de atendimento e um rodapé com o CRMV. As saídas vieram de
execuções reais no Node 24.16.0 com React 19.2.8, usando
react-dom/server ou react-dom/client dentro do jsdom 30.
Por que passar props por muitos componentes vira um problema?
Porque cada componente intermediário fica acoplado a dados que não usa. No painel, o tema nasce no componente de cima e o botão que o troca está três níveis abaixo; sem contexto, o caminho é este:
import { useState } from 'react';
function BotaoTema({ tema, alternar }) {
return <button className={tema} onClick={alternar}>Trocar tema</button>;
}
function BarraDeAcoes({ tema, alternar }) {
return <div className="acoes"><BotaoTema tema={tema} alternar={alternar} /></div>;
}
function Cabecalho({ tema, alternar }) {
return (
<header className={tema}>
<h2>Pata Amiga</h2>
<BarraDeAcoes tema={tema} alternar={alternar} />
</header>
);
}
function Painel() {
const [tema, setTema] = useState('claro');
const alternar = () => setTema((t) => (t === 'claro' ? 'escuro' : 'claro'));
return <Cabecalho tema={tema} alternar={alternar} />;
}Funciona, e a saída comprova isso. Leia o caminho de baixo para cima: Painel
possui o estado; Cabecalho e BarraDeAcoes recebem duas props; somente
BotaoTema usa essas props. Esse repasse por camadas recebe o nome de prop
drilling. Em português simples, é passar uma encomenda por várias mãos que não
precisam abri-la.
Contexto não torna props no React desnecessárias. Props continuam sendo a escolha mais clara quando pai e filho conversam diretamente. O incômodo aparece quando muitos intermediários apenas repassam o mesmo dado.
Experimente você mesmo
Antes de alterar o exemplo, preveja: se BarraDeAcoes deixar de receber tema
e alternar, o botão ainda conseguirá trocar o tema? Agora retire mentalmente
essas duas props apenas desse componente e siga o caminho do dado. A previsão
correta é não: a encomenda deixa de chegar ao botão. Essa dependência do
caminho inteiro é a dor que o contexto vai resolver.
Até aqui: prop drilling não significa que o código está quebrado; significa que componentes intermediários conheceram informações de que não precisam.
Como createContext, provider e useContext funcionam juntos?
createContext cria o canal, o provider oferece um valor a uma parte da árvore
e useContext lê, no consumidor, o valor do provider mais próximo. Veja as três
peças no menor exemplo completo:
import { createContext, useContext } from 'react';
const TemaContexto = createContext('claro');
function BotaoTema() {
const tema = useContext(TemaContexto);
return <button className={`botao botao-${tema}`}>Trocar tema</button>;
}
function Cabecalho() {
return (
<header>
<h2>Pata Amiga</h2>
<BotaoTema />
</header>
);
}
function Painel() {
return (
<TemaContexto.Provider value="escuro">
<Cabecalho />
</TemaContexto.Provider>
);
}Leia o resultado como o usuário o receberia: o cabeçalho apareceu e o botão
ganhou a classe botao-escuro. Cabecalho não recebeu nem repassou o tema. O
BotaoTema, que realmente precisa do dado, sintonizou o canal com useContext.
O texto 'claro' passado a createContext é o valor default, ou valor de
reserva. Ele só será usado se não houver provider correspondente acima. Quando
existe mais de um provider do mesmo contexto, vale o mais próximo do consumidor
— como ouvir o alto-falante do seu andar, não o de outro bloco.
Como usar o contexto diretamente no React 19?
No React 19, você pode oferecer o valor com <TemaContexto value={...}>; em
versões anteriores, use <TemaContexto.Provider value={...}>. O teste abaixo
compara as duas formas no mesmo ambiente:
const TemaContexto = createContext('claro');
function BotaoTema() {
return <button>{useContext(TemaContexto)}</button>;
}
console.log('React', React.version);
console.log(renderToStaticMarkup(
<TemaContexto value="escuro"><BotaoTema /></TemaContexto>
));
console.log(renderToStaticMarkup(
<TemaContexto.Provider value="escuro"><BotaoTema /></TemaContexto.Provider>
));
console.log('Provider === contexto?', TemaContexto.Provider === TemaContexto);
console.log(String(TemaContexto.$$typeof), String(TemaContexto.Provider.$$typeof));A primeira e a segunda renderização produziram o mesmo botão. Neste React
19.2.8, o log também mostrou que TemaContexto.Provider e TemaContexto apontam
para o mesmo objeto. Esse detalhe explica a saída medida, mas o que o seu código
deve usar como contrato público é mais simples: React 19 aceita o contexto como
provider; React 18 exige a propriedade .Provider.
Em uma base nova que roda React 19, a forma curta reduz ruído. Em uma biblioteca
que precisa aceitar React 18, mantenha .Provider enquanto essa compatibilidade
for necessária.
Por que um objeto novo no provider pode causar novos renders?
Porque o React compara o value antigo e o novo com Object.is. Dois objetos
literais com o mesmo conteúdo ainda são referências diferentes; se o provider
renderizar de novo, os consumidores daquele contexto recebem a atualização.
Para enxergar o efeito sem adivinhar, os próximos exemplos usam um contador de render: uma variável do módulo registra quantas vezes cada componente foi calculado para produzir a interface.
const renders = {};
const conta = (nome) => { renders[nome] = (renders[nome] ?? 0) + 1; };
const mostrar = (quando) => console.log(quando, JSON.stringify(renders));Neste exemplo, o provider guarda duas coisas separadas — o tema e o número de
alertas da recepção. O Rodape só se importa com o tema, e ainda por cima está
envolvido em memo:
function ProvedorDeTema({ children }) {
const [tema, setTema] = useState('claro');
const [alertas, setAlertas] = useState(0);
const valor = { tema, alternar: () => setTema((t) => (t === 'claro' ? 'escuro' : 'claro')) };
return (
<TemaContexto.Provider value={valor}>
<p>{alertas} alertas</p>
<button id="alerta" onClick={() => setAlertas((n) => n + 1)}>Novo alerta</button>
{children}
</TemaContexto.Provider>
);
}
const Rodape = memo(function Rodape() {
conta('Rodape');
const { tema } = useContext(TemaContexto);
return <footer className={tema}>CRMV 12345</footer>;
});Experimente você mesmo
Antes de executar o roteiro, preveja o placar do Rodape: o tema não muda, mas
o contador de alertas muda duas vezes. Você espera 1, 1, 1 ou 1, 2,
3? Depois acompanhe os dois cliques em Novo alerta e confronte sua resposta
com a saída. act() apenas garante que cada atualização terminou antes da
leitura.
const raiz = createRoot(document.getElementById('raiz'));
await act(() => { raiz.render(<ProvedorDeTema><Rodape /></ProvedorDeTema>); });
mostrar('montou: ');
await act(() => { document.getElementById('alerta').click(); });
mostrar('1 alerta novo: ');
await act(() => { document.getElementById('alerta').click(); });
mostrar('2 alertas novos: ');A resposta medida foi 1, 2, 3. O tema não mudou, mas cada alerta
renderizou ProvedorDeTema novamente; o objeto literal de valor ganhou outra
referência. Para Object.is, o novo objeto é diferente do anterior, então o
React notificou os componentes que escutam esse contexto.
memo compara props e pode evitar renders provocados por props iguais. Ele não
impede que o próprio Rodape seja atualizado quando o contexto que consome
recebe outro valor. No prédio, é como fechar a porta do escritório: isso reduz o
barulho do corredor, mas não desliga o aparelho sintonizado no aviso.
Neste caso medido, alertas são alheios ao tema. Faz sentido estabilizar a função
com useCallback e o objeto com useMemo. O roteiro de cliques continua igual;
somente o provider muda:
function ProvedorDeTema({ children }) {
const [tema, setTema] = useState('claro');
const [alertas, setAlertas] = useState(0);
const alternar = useCallback(() => {
setTema((t) => (t === 'claro' ? 'escuro' : 'claro'));
}, []);
const valor = useMemo(() => ({ tema, alternar }), [tema, alternar]);
return (
<TemaContexto.Provider value={valor}>
<p>{alertas} alertas</p>
<button id="alerta" onClick={() => setAlertas((n) => n + 1)}>Novo alerta</button>
{children}
</TemaContexto.Provider>
);
}O Rodape permaneceu em 1 render; o provider ainda processou os cliques,
mas deixou de anunciar um valor de tema falsamente novo. alternar pode ficar
estável porque a função setTema, devolvida por useState, tem identidade
estável.
Isso não cria a regra de colocar todo objeto de provider em useMemo. Use essa
otimização quando objetos ou funções mudam de identidade em renders alheios ao
valor e a medição mostra trabalho desnecessário. Um valor primitivo ou um
provider que só renderiza quando o dado compartilhado realmente muda pode não
ganhar nada com a complexidade extra. O artigo de useMemo e
useCallback aprofunda esse critério.
Até aqui: a comparação acontece no value completo; memo não bloqueia
uma atualização de contexto, e memorizar só compensa quando existe uma mudança
de identidade irrelevante para eliminar.
Quais componentes re-renderizam quando o contexto muda?
Os componentes que consomem aquele contexto recebem a atualização quando o
provider oferece um valor diferente segundo Object.is. Componentes que não o
consomem podem ficar parados, desde que nenhuma outra causa — como novas props
do pai — os faça renderizar.
Agora o painel inteiro usa o provider estabilizado da seção anterior. Quatro
componentes leem o contexto; TabelaDeVacinas não lê e está ali como vizinha:
function ProvedorDeTema({ children }) {
const [tema, setTema] = useState('claro');
const alternar = useCallback(() => {
setTema((t) => (t === 'claro' ? 'escuro' : 'claro'));
}, []);
const valor = useMemo(() => ({ tema, alternar }), [tema, alternar]);
return <TemaContexto.Provider value={valor}>{children}</TemaContexto.Provider>;
}
function Cabecalho() {
conta('Cabecalho');
const { tema } = useContext(TemaContexto);
return <h2 className={tema}>Pata Amiga</h2>;
}
function BotaoTema() {
conta('BotaoTema');
const { alternar } = useContext(TemaContexto);
return <button id="trocar" onClick={alternar}>Trocar tema</button>;
}
function FilaDeAtendimento() {
conta('FilaDeAtendimento');
const { tema } = useContext(TemaContexto);
return <ul className={tema}><li>Nina — 14:30</li><li>Thor — 15:00</li></ul>;
}
function Rodape() {
conta('Rodape');
const { tema } = useContext(TemaContexto);
return <footer className={tema}>CRMV 12345</footer>;
}
function TabelaDeVacinas() {
conta('TabelaDeVacinas');
return <table><tbody><tr><td>V10</td><td>anual</td></tr></tbody></table>;
}O roteiro monta o painel e clica duas vezes no botão de trocar tema. Repare que
o clique é buscado pelo id: se você pegar o primeiro <button> da página, corre
o risco de clicar em outro e medir a coisa errada.
function Painel() {
return (
<ProvedorDeTema>
<Cabecalho />
<BotaoTema />
<FilaDeAtendimento />
<Rodape />
<TabelaDeVacinas />
</ProvedorDeTema>
);
}
const clicar = () => act(() => { document.getElementById('trocar').click(); });
const raiz = createRoot(document.getElementById('raiz'));
await act(() => { raiz.render(<Painel />); });
mostrar('montou: ');
await clicar();
mostrar('1 clique:');
await clicar();
mostrar('2 cliques:');Leia o placar em três passos:
TabelaDeVacinaspermaneceu em 1. Neste desenho, ela chega ao provider como o mesmo elemento emchildren, criado peloPainel. Como não consome o contexto nem recebe novas props, a troca de tema não exige outro render dela.childrenajudou aqui, mas não é um escudo universal: outro pai ou outra prop ainda poderia provocar um render.- Os quatro consumidores chegaram a 3. A API nativa de
useContextnão recebe um seletor de campos. Quando ovaluemuda, cada componente que chamauseContext(TemaContexto)é notificado, mesmo que aproveite apenas uma parte do objeto. BotaoTematambém chegou a 3. Ele usa somentealternar, cuja referência ficou estável, mas está sintonizado no mesmo canal que carregatema. Separar os dois canais elimina esse trabalho específico.
Como separar valores e ações em dois contextos?
Crie um contexto para o dado que muda e outro para as funções estáveis. Assim, um componente que apenas dispara uma ação não precisa ouvir cada mudança do dado. No painel, o tema ocupa um canal e as ações ocupam outro:
const TemaValor = createContext('claro');
const TemaAcoes = createContext(null);
function ProvedorDeTema({ children }) {
const [tema, setTema] = useState('claro');
const acoes = useMemo(() => ({
alternar: () => setTema((t) => (t === 'claro' ? 'escuro' : 'claro')),
definir: (novo) => setTema(novo),
}), []);
return (
<TemaAcoes.Provider value={acoes}>
<TemaValor.Provider value={tema}>{children}</TemaValor.Provider>
</TemaAcoes.Provider>
);
}
function BotaoTema() {
conta('BotaoTema');
const { alternar } = useContext(TemaAcoes);
return <button id="trocar" onClick={alternar}>Trocar tema</button>;
}Os outros três componentes só trocam useContext(TemaContexto) por
useContext(TemaValor). O roteiro de cliques é o mesmo da seção anterior:
O BotaoTema permaneceu em 1 render depois de dois cliques. Ele fala no
canal das ações, mas não escuta o canal do tema. Cabecalho,
FilaDeAtendimento e Rodape chegaram a 3 porque exibem o tema e realmente
precisam acompanhar as mudanças.
O array de dependências do useMemo das ações está vazio neste exemplo porque
as funções usam apenas setTema, cuja identidade é estável. Se uma ação passar
a depender de uma prop ou de outro valor reativo, essa dependência deverá entrar
no array. Não copie [] como receita sem conferir o corpo das funções.
Até aqui: dividir contextos funciona como separar dois sistemas de som. Quem precisa apenas enviar comandos não precisa ouvir os avisos de tema.
O que acontece quando um componente usa contexto sem provider?
Ele recebe o valor default passado a createContext; se esse default for
null ou undefined e o código esperar um objeto, a leitura falha. Primeiro,
veja o caso em que existe um objeto de reserva:
const TemaContexto = createContext({ tema: 'claro', alternar: () => {} });
function Rodape() {
const { tema } = useContext(TemaContexto);
return <footer className={tema}>CRMV 12345</footer>;
}
console.log(renderToStaticMarkup(<Rodape />));
console.log(renderToStaticMarkup(
<TemaContexto value={{ tema: 'escuro', alternar: () => {} }}>
<Rodape />
</TemaContexto>
));A primeira linha usou o default claro; a segunda encontrou o provider e usou
escuro. A reserva pode ser conveniente em um contexto com fallback real, mas
também pode esconder um provider esquecido: uma parte da tela volta ao tema
claro sem mostrar erro.
Uma alternativa é escolher null como sentinela — um valor deliberado que marca
“provider ausente”. Assim o esquecimento deixa uma pista imediata:
const TemaContexto = createContext(null);
function Rodape() {
const { tema } = useContext(TemaContexto);
return <footer className={tema}>CRMV 12345</footer>;
}
// o Rodape ficou fora do provider
console.log(renderToStaticMarkup(<Rodape />));TypeError: Cannot destructure property ‘tema’ of ‘useContext(…)’ as it is null. at Rodape (file:///private/tmp/pata-amiga/Rodape.mjs:6:11) at Object.react_stack_bottom_frame (/private/tmp/pata-amiga/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9808:18) at renderWithHooks (/private/tmp/pata-amiga/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5062:19) at renderElement (/private/tmp/pata-amiga/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5497:23) … (mais 6 quadros, do react-dom até a chamada em Rodape.mjs) Node.js v24.16.0
Esse TypeError vem da desestruturação do JavaScript: o código tentou retirar
tema de null. O sintoma é a falha em Rodape; a causa é o componente estar
fora do provider; a primeira verificação é subir pela árvore e localizar onde
ProvedorDeTema foi montado. Se o acesso fosse contexto.tema, a mensagem seria
Cannot read properties of null, mas a causa continuaria igual.
A correção mais clara é centralizar a leitura em um Hook próprio. Ele valida a sentinela e troca o erro genérico por uma instrução útil:
const TemaContexto = createContext(undefined);
function useTema() {
const valor = useContext(TemaContexto);
if (valor === undefined) {
throw new Error('useTema() precisa de um <ProvedorDeTema> acima na árvore');
}
return valor;
}
function Rodape() {
const { tema } = useTema();
return <footer className={tema}>CRMV 12345</footer>;
}Agora o rastro aponta useTema e informa que falta <ProvedorDeTema> acima.
Expor apenas useTema e ProvedorDeTema, mantendo TemaContexto privado,
ajuda todos os consumidores a passar pela mesma validação.
Context API é um gerenciador de estado?
Não. Context API transporta um valor pela árvore; quem guarda e atualiza o dado
pode ser useState, useReducer, uma prop ou outra fonte. Neste exemplo, o
contexto recebe sempre a mesma referência e a mutação não agenda render algum:
const TemaContexto = createContext(null);
// objeto solto no módulo: o contexto só carrega a referência dele
const configuracao = { tema: 'claro' };
function Rodape() {
const { tema } = useContext(TemaContexto);
return <footer className={tema}>CRMV 12345</footer>;
}
function Painel() {
return (
<TemaContexto value={configuracao}>
<button onClick={() => { configuracao.tema = 'escuro'; }}>Trocar tema</button>
<Rodape />
</TemaContexto>
);
}
const raiz = createRoot(document.getElementById('raiz'));
await act(() => { raiz.render(<Painel />); });
console.log('antes: ', document.querySelector('footer').outerHTML);
await act(() => { document.querySelector('button').click(); });
console.log('objeto:', JSON.stringify(configuracao));
console.log('depois:', document.querySelector('footer').outerHTML);O objeto passou a conter escuro, como mostra a segunda linha, mas o rodapé
continuou com a classe claro. Mutar uma referência não avisa o React nem
substitui o value; é a mesma base de atualizar estado sem
mutar. useState é uma forma simples de
guardar esse dado e solicitar a nova renderização, como mostra a lição de
estado no React.
Aqui termina a analogia do alto-falante: contexto leva o aviso aos andares, mas não escreve nem atualiza o aviso. Seu papel é substituir o valor em um escopo da árvore. Providers aninhados demonstram isso; o mais próximo do consumidor vence:
const TemaContexto = createContext('claro');
function Etiqueta({ texto }) {
return <span className={useContext(TemaContexto)}>{texto}</span>;
}
console.log(renderToStaticMarkup(
<TemaContexto value="escuro">
<Etiqueta texto="painel" />
<TemaContexto value="alto-contraste">
<Etiqueta texto="modal de emergencia" />
</TemaContexto>
<Etiqueta texto="rodape" />
</TemaContexto>
));Leia a ordem: a etiqueta painel ouviu escuro do provider externo; a etiqueta
do modal encontrou primeiro o provider interno e recebeu alto-contraste; ao
sair desse escopo, rodape voltou a ouvir escuro. Um provider interno não
altera o restante do prédio — apenas a subárvore abaixo dele.
Com isso na mão, dá para decidir com critério:
| situação | ferramenta | por quê |
|---|---|---|
| duas telas irmãs precisam do mesmo dado | subir o estado para o pai comum | não precisa de canal nenhum |
| um componente no meio só repassa a prop | children e composição |
resolve prop drilling sem re-render extra |
| tema, idioma, usuário logado, moeda | Context API | muda pouco, é lido em muitos lugares |
| muitas transições no mesmo estado | useReducer dentro do provider | centraliza as regras, e o dispatch é estável |
| lista grande que muda a cada tecla | biblioteca de estado com seletor | contexto acorda todos os consumidores |
A última linha marca o limite. A API nativa de useContext não oferece um
seletor para pedir “avise apenas quando tema mudar”. Quando o value muda,
todos os consumidores daquele contexto são notificados. Se isso aparece como
custo real no Profiler — o medidor do React DevTools —, divida os contextos ou
avalie uma ferramenta que ofereça seletores. useMemo evita mudanças de
identidade alheias; ele não elimina uma atualização legítima do valor
compartilhado.
O que estudar depois da Context API?
O próximo passo é combinar o provider com useReducer: o reducer concentra as
regras de mudança e o dispatch mantém identidade estável. Depois, o artigo
sobre re-render no React explica como o React
decide recalcular componentes. A trilha de React organiza
esses assuntos; se o clique e a atualização ainda parecerem mágicos, revise antes
os eventos do React.
Experimente você mesmo
Antes de montar o teste final, preveja: se um consumidor lê tema e outro lê
usuario no mesmo objeto de contexto, qual deles renderiza quando somente o
tema muda? Registre ambos com console.count, altere o tema e confira. Depois
divida os valores em dois contextos e repita. Com a API nativa de useContext,
a previsão é: os dois recebem a primeira atualização; separados, o consumidor
de usuário deixa de ser atualizado por causa do contexto de tema. Ele ainda
pode renderizar por outro motivo, como uma nova prop ou um pai que também
renderizou.
Essa comparação fecha a ideia do alto-falante: primeiro escolha quem realmente precisa ouvir o canal; só depois pense em estabilizar ou separar a transmissão.
Perguntas frequentes
Context API substitui Redux ou Zustand?
Posso usar vários contextos no mesmo projeto?
Onde eu coloco o provider?
useContext funciona dentro de if ou de laço?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 com React 19.2.8, react-dom 19.2.8 e jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — useContext — react.dev
- React — createContext — react.dev
- React — Passing Data Deeply with Context — react.dev


