Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

children e composição no React: o fim do prop drilling

Como um componente embrulha outro com props.children, o padrão de slots com props de elemento e por que isso resolve prop drilling sem contexto.

Rodolfo Mori15 min de leitura

Você cria um cartão bonito para uma consulta. Depois pedem outro título, dois botões e um formulário no mesmo cartão. Quando percebe, o componente tem tantas props e condições que uma mudança pequena parece desmontar um armário inteiro. A dor não está no JSX: o cartão está decidindo coisas demais.

Neste guia, você vai separar a estrutura do componente do conteúdo que entra nele. O nome dessa técnica é composição. Primeiro faremos uma moldura mínima; depois, aplicaremos a mesma ideia ao painel de uma clínica veterinária, comparando renders reais e corrigindo os erros mais comuns.

Pense numa moldura de fotos. A moldura escolhe borda, tamanho e posição, mas o dono escolhe a foto. No React, o componente é a moldura e children é o pacote de conteúdo entregue a ela. A comparação tem um limite: children não é uma foto física nem uma cópia pronta do HTML. É uma estrutura opaca administrada pelo React, que pode representar elementos, texto, listas ou outros valores.

Todos os exemplos usam a PetLar: uma agenda, cartões de pets e uma janela de remarcação. Um componente é uma função que descreve uma parte da interface; uma prop é uma entrada recebida por essa função; e JSX é a escrita parecida com HTML usada para descrever os elementos. O código foi executado no Node 24.16.0 com React 19.2.8 e jsdom 30.0.1. Quando um bloco imprime um resultado, a saída real aparece logo abaixo dele.

Esse padrão também existe fora desta clínica fictícia. O Dialog do Material UI, usado por equipes que constroem produtos em React, recebe children e oferece subcomponentes para título, conteúdo e ações. A equipe reutiliza a mesma casca de diálogo em confirmações, formulários e avisos sem ensinar à casca todos os conteúdos possíveis. A API oficial do Dialog documenta esse contrato.

Para reproduzir os exemplos, crie uma pasta vazia e execute, nesta ordem, npm init -y, npm pkg set type=module, npm install react react-dom jsdom e npm install -D tsx. Salve o bloco que estiver estudando como exemplo.jsx e rode npx tsx exemplo.jsx. Nos blocos de medição, mantenha também o preparo indicado no texto.

O que é props.children e por que ele fica entre as tags?

props.children é a prop que recebe o conteúdo colocado entre a abertura e o fechamento de um componente. Ao renderizar {children}, a moldura escolhe onde encaixar esse conteúdo, enquanto quem usa a moldura escolhe o que vai dentro dela.

renderToStaticMarkup é a função usada aqui para transformar a árvore JSX numa string HTML. Ela serve para enxergar o resultado exato sem abrir uma página no navegador; não faz parte do mecanismo de children.

jsx
import { renderToStaticMarkup } from 'react-dom/server';

function Cartao({ children }) {
  return <section className="cartao">{children}</section>;
}

const marcacao = renderToStaticMarkup(
  <Cartao>
    <h3>Thor — Golden Retriever</h3>
    <p>Consulta de rotina, 14h30</p>
  </Cartao>
);

console.log(marcacao);
<section class="cartao"><h3>Thor — Golden Retriever</h3><p>Consulta de rotina, 14h30</p></section>

Repare na divisão de responsabilidades. Quem usa Cartao escreveu o <h3> e o <p>; o próprio Cartao decidiu apenas que tudo ficaria em uma section com a classe cartao. Isso é composição: montar a tela juntando peças, em vez de tentar configurar uma peça gigante por props.

Experimente você mesmo

No arquivo exemplo.jsx preparado no início, copie o primeiro bloco e troque apenas o texto e a ordem do <h3> e do <p> dentro de Cartao. Antes de executar, preveja qual elemento aparecerá primeiro e se a section perderá a classe cartao. Rode npx tsx exemplo.jsx e compare. A prática deu certo se o conteúdo mudou, mas a moldura continuou igual.

Até aqui

O caso mínimo tem duas decisões separadas: Cartao cuida da estrutura e quem o utiliza fornece o conteúdo. Essa separação é a base dos exemplos reais a seguir.

O que pode chegar dentro de children?

children pode representar texto, um elemento, vários elementos ou nenhum conteúdo. Porém, o React trata essa prop como uma estrutura opaca: observe os resultados abaixo, mas não escreva lógica dependendo do formato interno dela.

jsx
function Espiao({ children }) {
  const tipo = Array.isArray(children) ? `array de ${children.length}` : typeof children;
  console.log('children é', tipo);
  return null;
}

renderToStaticMarkup(<Espiao>Consulta de rotina</Espiao>);
renderToStaticMarkup(<Espiao><p>Thor</p></Espiao>);
renderToStaticMarkup(<Espiao><p>Thor</p><p>Mel</p></Espiao>);
renderToStaticMarkup(<Espiao />);
children é string children é object children é array de 2 children é undefined

As quatro chamadas produziram quatro observações diferentes. Neste teste, um filho apareceu como objeto, dois apareceram como array e a ausência virou undefined. Isso explica por que children.map(...) pode funcionar com dois filhos e quebrar com um só. Ainda assim, esses formatos não formam um contrato para você manipular por conta própria.

Por que o prop drilling aparece em quatro níveis?

O prop drilling aparece quando uma prop atravessa componentes que apenas a repassam até chegar a quem realmente a utiliza. No painel da PetLar, o contador de consultas confirmadas mora no fim da árvore, enquanto o estado vive no topo. Quatro níveis viram entregadores de um pacote que não abrem.

Este é o preparo, e ele vale para todos os blocos com medição daqui para baixo:

jsx
import { useState, useRef, useContext, createContext, act } from 'react';
import { createRoot } from 'react-dom/client';
import { JSDOM } from 'jsdom';

const dom = new JSDOM('<!doctype html><body><div id="raiz"></div>');
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const raiz = document.getElementById('raiz');

let renderizados = [];
const marcar = (nome) => renderizados.push(nome);

Com o DOM de pé, a árvore com prop drilling:

jsx
function App() {
  const [confirmadas, setConfirmadas] = useState(0);
  marcar('App');
  return (
    <PainelDoDia
      confirmadas={confirmadas}
      aoConfirmar={() => setConfirmadas((n) => n + 1)}
    />
  );
}

function PainelDoDia({ confirmadas, aoConfirmar }) {
  marcar('PainelDoDia');
  return (
    <div>
      <AgendaDaSemana />
      <ColunaDeHoje confirmadas={confirmadas} aoConfirmar={aoConfirmar} />
    </div>
  );
}

function AgendaDaSemana() {
  marcar('AgendaDaSemana');
  return <p>Seg a sex, 8h-18h</p>;
}

function ColunaDeHoje({ confirmadas, aoConfirmar }) {
  marcar('ColunaDeHoje');
  return <ListaDeConsultas confirmadas={confirmadas} aoConfirmar={aoConfirmar} />;
}

function ListaDeConsultas({ confirmadas, aoConfirmar }) {
  marcar('ListaDeConsultas');
  return <SeloDeConfirmadas confirmadas={confirmadas} aoConfirmar={aoConfirmar} />;
}

function SeloDeConfirmadas({ confirmadas, aoConfirmar }) {
  marcar('SeloDeConfirmadas');
  return (
    <p>
      {confirmadas} confirmadas
      <button onClick={aoConfirmar}>Confirmar</button>
    </p>
  );
}

const root = createRoot(raiz);
await act(async () => root.render(<App />));
console.log('render inicial:', renderizados.join(' > '));

renderizados = [];
await act(async () => raiz.querySelector('button').click());
console.log('depois do clique:', renderizados.join(' > '));
console.log('componentes rodados de novo:', renderizados.length);
render inicial: App > PainelDoDia > AgendaDaSemana > ColunaDeHoje > ListaDeConsultas > SeloDeConfirmadas depois do clique: App > PainelDoDia > AgendaDaSemana > ColunaDeHoje > ListaDeConsultas > SeloDeConfirmadas componentes rodados de novo: 6

Um clique fez seis funções de componente rodarem novamente. Até AgendaDaSemana, que não recebe nem lê confirmadas, está na lista porque faz parte da subárvore de um componente que renderizou de novo.

É importante separar as causas: passar props não cobra, sozinho, um “imposto de render”. O prop drilling cria principalmente acoplamento e repetição; neste exemplo, os seis renders também dependem de onde o estado mora e de como a árvore foi montada. A composição vai reorganizar exatamente essas responsabilidades.

Até aqui

O problema tem dois sinais: componentes intermediários conhecem uma prop que não usam e uma atualização no topo percorre esta árvore inteira. Agora temos um número inicial — seis — para comparar com a versão composta.

Como a composição reduz seis renders para um neste exemplo?

Neste exemplo, a composição reduz os seis renders ao mover o estado para PainelDoDia e fazer o App criar o conteúdo independente antes de entregá-lo como children. É uma mudança de lugar e responsabilidade, não uma API nova.

jsx
function App() {
  marcar('App');
  return (
    <PainelDoDia>
      <AgendaDaSemana />
      <ListaDeConsultas />
    </PainelDoDia>
  );
}

function PainelDoDia({ children }) {
  const [confirmadas, setConfirmadas] = useState(0);
  marcar('PainelDoDia');
  return (
    <div>
      <p>
        {confirmadas} confirmadas
        <button onClick={() => setConfirmadas((n) => n + 1)}>Confirmar</button>
      </p>
      {children}
    </div>
  );
}

function AgendaDaSemana() {
  marcar('AgendaDaSemana');
  return <p>Seg a sex, 8h-18h</p>;
}

function ListaDeConsultas() {
  marcar('ListaDeConsultas');
  return <p>Thor, Mel e Pipoca</p>;
}

const root = createRoot(raiz);
await act(async () => root.render(<App />));
console.log('render inicial:', renderizados.join(' > '));

renderizados = [];
await act(async () => raiz.querySelector('button').click());
console.log('depois do clique:', renderizados.join(' > '));
console.log('componentes rodados de novo:', renderizados.length);
console.log('html:', raiz.innerHTML);
render inicial: App > PainelDoDia > AgendaDaSemana > ListaDeConsultas depois do clique: PainelDoDia componentes rodados de novo: 1 html: <div><p>1 confirmadas<button>Confirmar</button></p><p>Seg a sex, 8h-18h</p><p>Thor, Mel e Pipoca</p></div>

O resultado foi de seis para um nesta árvore, nesta interação e nesta medição. AgendaDaSemana e ListaDeConsultas continuam dentro de PainelDoDia no HTML, mas foram criados pelo App. Como o clique não fez o App renderizar de novo, PainelDoDia recebeu os mesmos elementos que já tinha recebido.

Isso não significa que composição sempre produz “um render”. O número depende de quem guarda o estado, de quem cria os elementos e de quais valores cada filho consome. A vantagem garantida aqui é uma árvore com responsabilidades mais claras; a redução medida é consequência dessa organização específica.

Experimente você mesmo

No mesmo exemplo.jsx, mantenha o bloco de preparo, use a segunda árvore e mova <ListaDeConsultas /> para dentro de PainelDoDia, logo abaixo do botão, em vez de recebê-lo por children. Antes de alterar, preveja se ele voltará a aparecer na lista “depois do clique”. Rode npx tsx exemplo.jsx. A resposta esperada é “sim”, porque o componente que cria esse elemento renderiza novamente quando confirmadas muda.

Por que o filho não roda de novo neste caso?

O filho não roda novamente neste caso porque <ListaDeConsultas /> foi criado pelo App, e somente PainelDoDia renderizou após o clique. Assim, o objeto de elemento recebido em children manteve a mesma identidade e o React pôde reaproveitar aquela subárvore.

jsx
function App() {
  return (
    <PainelDoDia>
      <ListaDeConsultas />
    </PainelDoDia>
  );
}

function PainelDoDia({ children }) {
  const [confirmadas, setConfirmadas] = useState(0);
  const anterior = useRef(null);

  if (anterior.current === null) {
    console.log('render 1: primeira vez, nada para comparar');
  } else {
    console.log(
      `render ${confirmadas + 1}: children é o mesmo objeto?`,
      Object.is(anterior.current, children)
    );
  }
  anterior.current = children;

  return (
    <div>
      <button onClick={() => setConfirmadas((n) => n + 1)}>Confirmar</button>
      {children}
    </div>
  );
}

function ListaDeConsultas() {
  console.log('   ListaDeConsultas rodou');
  return <p>Thor, Mel e Pipoca</p>;
}

const root = createRoot(raiz);
await act(async () => root.render(<App />));
await act(async () => raiz.querySelector('button').click());
await act(async () => raiz.querySelector('button').click());
render 1: primeira vez, nada para comparar ListaDeConsultas rodou render 2: children é o mesmo objeto? true render 3: children é o mesmo objeto? true

Object.is devolve true nos dois cliques, e ListaDeConsultas rodou aparece somente no render inicial. Voltando à moldura: trocar a legenda da moldura não obriga o fotógrafo a entregar outra foto. Mas esse atalho depende justamente disso. Se o App, que cria o elemento, também renderizar, ele normalmente criará outro objeto e essa identidade deixará de ser a mesma.

Não é uma promessa universal de desempenho nem um substituto automático para memo. É um comportamento desta relação entre criador e dono do estado. O artigo sobre re-render no React detalha os critérios envolvidos.

Como criar slots com props nomeadas?

Crie slots passando JSX em props com nomes como cabecalho, acoes e rodape. children funciona como o espaço principal; as props nomeadas dizem ao componente em qual espaço cada outra parte deve entrar.

jsx
function CartaoConsulta({ cabecalho, acoes, children }) {
  return (
    <article className="cartao">
      <header>{cabecalho}</header>
      <div className="corpo">{children}</div>
      <footer>{acoes ?? <button>Confirmar</button>}</footer>
    </article>
  );
}

console.log(
  renderToStaticMarkup(
    <CartaoConsulta
      cabecalho={<h3>Thor — 14h30</h3>}
      acoes={<><button>Remarcar</button><button>Cancelar</button></>}
    >
      <p>Golden Retriever, 4 anos. Vacina antirrábica.</p>
    </CartaoConsulta>
  )
);
<article class="cartao"><header><h3>Thor — 14h30</h3></header><div class="corpo"><p>Golden Retriever, 4 anos. Vacina antirrábica.</p></div><footer><button>Remarcar</button><button>Cancelar</button></footer></article>

O mesmo componente, agora só com cabecalho e children — sem a prop acoes, o ?? entrega o rodapé padrão:

jsx
console.log(
  renderToStaticMarkup(
    <CartaoConsulta cabecalho={<h3>Mel — 15h00</h3>}>
      <p>Poodle, 9 anos. Retorno.</p>
    </CartaoConsulta>
  )
);
<article class="cartao"><header><h3>Mel — 15h00</h3></header><div class="corpo"><p>Poodle, 9 anos. Retorno.</p></div><footer><button>Confirmar</button></footer></article>

O exemplo saiu de uma moldura com um espaço para um cartão real com regiões bem definidas. Props booleanas como temRemarcar, temCancelar e temConfirmar podem servir para variações pequenas e conhecidas. Quando as combinações crescem, porém, um slot permite trocar o conteúdo sem ensinar cada nova opção à casca.

O limite da analogia continua valendo: slot não é um buraco físico e não torna o componente infinitamente flexível. A casca ainda decide a ordem, a marcação e as regras de cada região; quem a utiliza decide apenas o conteúdo permitido ali.

Como um componente casca decide o que mostrar?

Um componente casca recebe partes prontas e decide se e onde elas entram na interface. No modal abaixo, Janela controla título, moldura e rodapé; o formulário de remarcação vem de fora como children.

jsx
function Janela({ aberta, titulo, children, rodape }) {
  console.log(`Janela "${titulo}" — rodou o corpo?`, aberta ? 'sim' : 'nao');
  if (!aberta) return null;
  return (
    <div className="janela">
      <h2>{titulo}</h2>
      <div>{children}</div>
      <footer>{rodape}</footer>
    </div>
  );
}

function FormularioDeRemarcacao() {
  console.log('  FormularioDeRemarcacao rodou');
  return (
    <form>
      <label htmlFor="data">Nova data</label>
      <input id="data" type="date" />
    </form>
  );
}

console.log('--- fechada ---');
console.log(
  renderToStaticMarkup(
    <Janela aberta={false} titulo="Remarcar consulta do Thor">
      <FormularioDeRemarcacao />
    </Janela>
  ) || '(nada no HTML)'
);

console.log('--- aberta ---');
console.log(
  renderToStaticMarkup(
    <Janela aberta titulo="Remarcar consulta do Thor" rodape={<button>Salvar</button>}>
      <FormularioDeRemarcacao />
    </Janela>
  )
);
--- fechada --- Janela "Remarcar consulta do Thor" — rodou o corpo? nao (nada no HTML) --- aberta --- Janela "Remarcar consulta do Thor" — rodou o corpo? sim FormularioDeRemarcacao rodou <div class="janela"><h2>Remarcar consulta do Thor</h2><div><form><label for="data">Nova data</label><input id="data" type="date"/></form></div><footer><button>Salvar</button></footer></div>

Com a janela fechada, FormularioDeRemarcacao rodou não aparece. Escrever <FormularioDeRemarcacao /> cria o objeto que descreve o elemento, mas não executa a função FormularioDeRemarcacao naquele instante. O React só executa essa função se a casca realmente devolver o elemento para a árvore. Portanto, “não rodou o corpo” não significa custo absolutamente zero; significa que a função do formulário e sua subárvore não foram renderizadas.

Até aqui

children atende ao espaço principal, slots nomeados atendem a regiões específicas e a casca continua dona do layout. Em todos os casos, o conteúdo é fornecido por fora sem obrigar a casca a conhecer os detalhes dele.

Quando children pode ser uma função e qual é o preço?

Use children como função quando o conteúdo precisa receber um valor que só a casca conhece. Esse padrão, também chamado de render prop, permite que a casca chame a função com seu estado e receba um novo elemento como resultado.

jsx
function PainelDoDia({ children }) {
  const [confirmadas, setConfirmadas] = useState(0);
  marcar('PainelDoDia');
  return (
    <div>
      <button onClick={() => setConfirmadas((n) => n + 1)}>Confirmar</button>
      {typeof children === 'function' ? children(confirmadas) : children}
    </div>
  );
}

function ListaDeConsultas({ confirmadas = 0 }) {
  marcar('ListaDeConsultas');
  return <p>Thor, Mel e Pipoca — {confirmadas} confirmadas</p>;
}

function AppElemento() {
  return (
    <PainelDoDia>
      <ListaDeConsultas />
    </PainelDoDia>
  );
}

function AppFuncao() {
  return (
    <PainelDoDia>
      {(confirmadas) => <ListaDeConsultas confirmadas={confirmadas} />}
    </PainelDoDia>
  );
}

async function medir(rotulo, App) {
  const root = createRoot(raiz);
  await act(async () => root.render(<App />));
  renderizados = [];
  await act(async () => raiz.querySelector('button').click());
  console.log(`${rotulo}: ${renderizados.join(' > ')}  (${renderizados.length})`);
  console.log(`${rotulo} html: ${raiz.innerHTML}`);
  await act(async () => root.unmount());
}

await medir('children como elemento', AppElemento);
await medir('children como função ', AppFuncao);
children como elemento: PainelDoDia (1) children como elemento html: <div><button>Confirmar</button><p>Thor, Mel e Pipoca — 0 confirmadas</p></div> children como função : PainelDoDia > ListaDeConsultas (2) children como função html: <div><button>Confirmar</button><p>Thor, Mel e Pipoca — 1 confirmadas</p></div>

Os números mostram a troca feita neste exemplo. Como elemento, a lista não lê o estado: PainelDoDia roda sozinho, mas o texto permanece em 0 confirmadas. Como função, PainelDoDia chama children(confirmadas) após a mudança; essa chamada cria o elemento de ListaDeConsultas novamente, o componente roda e o texto chega a 1 confirmadas.

Isso não quer dizer que toda função em children “custa exatamente um render”. O total depende do que a função devolve e de quais atualizações acontecem. A regra prática é mais simples: se o conteúdo precisa do valor da casca, a função é uma ponte legítima; se não precisa, passar o elemento pronto mantém as partes mais independentes.

Quando escolher composição, contexto ou estado global?

Escolha composição para consumidores próximos e contexto para um valor lido em vários pontos da mesma área. Um contexto também pode sobreviver à navegação se o provider estiver acima das rotas; uma store externa ou um cache passa a fazer sentido quando o dado tem ciclo de vida, sincronização ou regras que não combinam com aquela árvore. Nenhuma dessas opções persiste após recarregar a página sem configurar armazenamento local ou servidor. Para tornar a diferença concreta, o painel da PetLar mede três organizações no mesmo clique.

O exemplo mantém <ConfirmadasContexto.Provider> por compatibilidade com React 18. No React 19, a forma principal também pode ser escrita diretamente como <ConfirmadasContexto value={valor}>.

jsx
const ConfirmadasContexto = createContext(null);

function AppContexto() {
  marcar('App');
  return (
    <ProvedorDeConfirmadas>
      <PainelDoDia />
    </ProvedorDeConfirmadas>
  );
}

function ProvedorDeConfirmadas({ children }) {
  const [confirmadas, setConfirmadas] = useState(0);
  marcar('ProvedorDeConfirmadas');
  const valor = { confirmadas, confirmar: () => setConfirmadas((n) => n + 1) };
  return (
    <ConfirmadasContexto.Provider value={valor}>
      {children}
    </ConfirmadasContexto.Provider>
  );
}

function SeloDeConfirmadas() {
  marcar('SeloDeConfirmadas');
  const { confirmadas, confirmar } = useContext(ConfirmadasContexto);
  return (
    <p>
      {confirmadas} confirmadas <button onClick={confirmar}>Confirmar</button>
    </p>
  );
}

// a mesma corrente de quatro níveis da primeira medição, agora sem prop nenhuma
function PainelDoDia() {
  marcar('PainelDoDia');
  return (
    <div>
      <AgendaDaSemana />
      <ColunaDeHoje />
    </div>
  );
}

function ColunaDeHoje() {
  marcar('ColunaDeHoje');
  return <ListaDeConsultas />;
}

function ListaDeConsultas() {
  marcar('ListaDeConsultas');
  return <SeloDeConfirmadas />;
}

async function medir(rotulo, App) {
  const root = createRoot(raiz);
  await act(async () => root.render(<App />));
  const inicial = renderizados.length;
  renderizados = [];
  await act(async () => raiz.querySelector('button').click());
  console.log(
    `${rotulo.padEnd(13)} inicial: ${inicial}  |  no clique: ${renderizados.length}  (${renderizados.join(', ')})`
  );
  await act(async () => root.unmount());
  renderizados = [];
}

await medir('prop drilling', AppDrilling);
await medir('contexto', AppContexto);
await medir('composicao', AppComposto);

AppDrilling, AppComposto e AgendaDaSemana são exatamente os das seções anteriores, copiados para o mesmo arquivo. O medir monta uma versão de cada vez na mesma raiz, clica no botão, conta quem rodou e desmonta antes da próxima — por isso os três números saem da mesma execução, na mesma máquina.

prop drilling inicial: 6 | no clique: 6 (App, PainelDoDia, AgendaDaSemana, ColunaDeHoje, ListaDeConsultas, SeloDeConfirmadas) contexto inicial: 7 | no clique: 2 (ProvedorDeConfirmadas, SeloDeConfirmadas) composicao inicial: 4 | no clique: 1 (PainelDoDia)

Nesta medição, os resultados foram seis, dois e um. O contexto chegou a dois porque o provedor atualizou e o consumidor leu o novo valor; os elementos em children foram criados acima e mantiveram a identidade durante essa atualização do provedor. Esse resultado depende da forma como a árvore foi montada — receber children, sozinho, não garante o mesmo número em qualquer aplicação.

Composição não é rival da Context API. As duas podem trabalhar juntas: a composição reduz o conhecimento dos intermediários, enquanto o contexto entrega um valor aos consumidores que realmente o leem. A tabela abaixo resume decisões para este cenário medido, e não uma regra de benchmark universal.

situação escolha por quê
um consumidor só, perto de quem tem o estado composição resolve sem API nova e custa 1 render
filho precisa ler o estado da casca children como função paga 1 render a mais, mas o valor chega
muitos consumidores espalhados pela tela contexto com provedor que recebe children evita passar prop por níveis que não usam
estado que sobrevive a troca de rota, cache de servidor biblioteca de estado ou cache contexto re-renderiza todo consumidor a cada mudança

Quais erros costumam acontecer com composição?

Os dois erros mais comuns são esquecer de renderizar children e tratar um elemento como se fosse uma função. O primeiro faz o conteúdo sumir em silêncio; o segundo produz TypeError: children is not a function.

jsx
function Cartao({ titulo }) {
  return (
    <article className="cartao">
      <h3>{titulo}</h3>
    </article>
  );
}

console.log(
  renderToStaticMarkup(
    <Cartao titulo="Thor — 14h30">
      <p>Golden Retriever, 4 anos. Vacina antirrábica.</p>
    </Cartao>
  )
);
<article class="cartao"><h3>Thor — 14h30</h3></article>

O <p> foi passado e desapareceu. O diagnóstico começa no componente que deveria servir de moldura: confira se ele recebe children na assinatura e se devolve {children} no local desejado. Neste caso, a correção é adicionar children ao parâmetro de Cartao e renderizá-lo depois do título.

O segundo é barulhento: o componente espera children como função e recebe JSX.

jsx
function ListaDeConsultas({ consultas, children }) {
  return <ul>{consultas.map((c) => children(c))}</ul>;
}

const agendaDeHoje = [
  { id: 1, pet: 'Thor', hora: '14h30' },
  { id: 2, pet: 'Mel', hora: '15h00' },
];

renderToStaticMarkup(
  <ListaDeConsultas consultas={agendaDeHoje}>
    <li>Uma consulta</li>
  </ListaDeConsultas>
);
file:///private/tmp/petlar/lista.mjs:4 return /* @__PURE__ */ jsx("ul", { children: consultas.map((c) => children(c)) }); ^

TypeError: children is not a function at file:///private/tmp/petlar/lista.mjs:4:69 at Array.map (<anonymous>) at ListaDeConsultas (file:///private/tmp/petlar/lista.mjs:4:58) at Object.react_stack_bottom_frame (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9808:18) at renderWithHooks (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5062:19) at renderElement (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5497:23) at retryNode (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6417:31) at performWork (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7435:17) at startWork (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8270:7) at renderToStringImpl (/private/tmp/petlar/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8380:7)

Node.js v24.16.0

O .jsx virou .mjs no caminho porque o esbuild compilou o JSX antes de o Node rodar — é o jsx("ul", ...) que aparece na primeira linha. As três primeiras linhas do rastro são as suas; da quarta em diante é o React tentando renderizar o componente. Ler de cima para baixo e parar no primeiro caminho que é seu economiza tempo em qualquer stack trace de React.

A mensagem é literal: children existe, mas é um objeto de elemento, não uma função chamável. O diagnóstico é comparar o contrato do componente com a forma de uso. Há duas correções possíveis: quem usa ListaDeConsultas passa uma função que recebe cada consulta e devolve o item, ou o componente deixa de chamar children(c) e renderiza o elemento diretamente. Se as duas formas forem permitidas, faça a verificação de tipo mostrada antes de chamar a função.

Até aqui

Quando o conteúdo some, procure onde {children} deveria ser renderizado. Quando aparece “is not a function”, descubra se o contrato esperava um elemento ou uma função. A mensagem de erro aponta o tipo de desencontro; o componente e seu uso mostram qual lado deve ser corrigido.

O que a composição não resolve?

A composição não distribui estado global, não armazena dados do servidor e não impede renders que são necessários. Ela organiza quem monta cada parte da tela; outros problemas ainda pedem outras ferramentas.

Se o mesmo valor é lido em muitos pontos distantes, montar tudo no App pode transformá-lo num arquivo que conhece detalhes demais. Nesse caso, contexto ou uma biblioteca de estado pode entregar o valor sem uma longa corrente de props. Também vale lembrar que prop drilling não é, por si só, sinônimo de lentidão: o incômodo principal costuma ser o acoplamento dos intermediários.

Se o estado muda rápido e um consumidor precisa daquele valor, o consumidor continuará candidato a renderizar. useReducer pode organizar transições complexas, mas não elimina renders automaticamente; memorização também só deve entrar depois de medir o gargalo.

Por fim, o estado não precisa morar obrigatoriamente no mesmo componente do botão. Ele deve ficar no ancestral comum mais próximo de quem lê e de quem altera o valor. A moldura ajuda a organizar as fotos, mas não funciona como correio para dez salas nem acelera a revelação de uma foto pesada — esse é o limite prático da analogia.

Qual é o próximo passo depois da composição?

O próximo passo é aplicar a composição em uma tela pequena e, depois, avançar para o React Router, assunto de ordem 19 da trilha. Na navegação, a mesma ideia reaparece: o layout funciona como casca e cada rota entrega seu conteúdo.

Comece contando quais componentes rodam após uma interação — um console.log no topo de cada função já revela o caminho. Antes de mudar a estrutura, preveja quais nomes deveriam desaparecer da lista. Em seguida, escolha um intermediário que apenas repassa uma prop, entregue o consumidor pronto por children ou por um slot e repita o teste.

A prática está concluída quando a tela final permanece igual, o intermediário deixa de conhecer a prop e sua nova contagem confirma a previsão. Depois, siga a trilha de React para estudar a navegação e consulte o guia de React sempre que quiser revisar a ordem completa.

  • react
  • children
  • composicao
  • prop drilling
  • componentes

Perguntas frequentes

children é uma prop como qualquer outra?
É. A única diferença é que o JSX preenche essa prop sozinho com o que está entre a tag de abertura e a de fecho. Escrever <Cartao children={<p>oi</p>} /> funciona e dá exatamente o mesmo resultado, só que ninguém escreve assim.
Posso passar mais de um bloco de conteúdo para o mesmo componente?
Pode, e o caminho é a prop nomeada de elemento, que costumam chamar de slot: cabecalho, acoes, rodape. children é o slot principal; os outros ganham nome porque o componente precisa saber onde encaixar cada um.
Composição substitui a Context API?
Nem sempre. Composição resolve quando existe um consumidor só, ou quando os consumidores estão perto uns dos outros. Quando o mesmo valor é lido em pontos distantes da tela, o contexto continua sendo a ferramenta — e a composição entra para segurar o provedor, evitando que a subárvore inteira renderize de novo.
Por que meu componente filho continua renderizando mesmo com children?
Porque alguma coisa acima dele renderizou de novo e recriou o elemento. O truque só funciona quando quem cria o elemento de children não renderiza junto com quem tem o estado.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, React 19.2.8 e jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Passando JSX como children — pt-br.react.dev
  2. React — Extraindo o estado para um componente que recebe children — pt-br.react.dev
  3. React — createRoot — pt-br.react.dev

Continue por aqui