DEV Community

Cover image for Compartilhando props em Compound Components sem usar Context API
José Lucas Panizio
José Lucas Panizio

Posted on

Compartilhando props em Compound Components sem usar Context API

TL;DR
O useEnhanceChildren é um hook customizado que permite injetar ou mesclar props do componente pai em seus filhos — ideal para compound components sem precisar usar Context API.

✅ Sem boilerplate, com tipagem forte e suporte a hierarquias aninhadas.
🔗 Veja o projeto completo no GitHub →


Quem já criou um compound component em React sabe que compartilhar props entre o componente pai e seus filhos pode ser um processo moroso.

A abordagem mais comum é usar a Context API, mas, sinceramente... sinto que estou usando uma bazuca pra matar uma formiga. Tudo o que eu queria era que os subcomponentes do Root tivessem acesso às mesmas props — sem precisar criar contextos, providers e hooks para cada caso.


😫 O problema

Existem basicamente duas formas tradicionais de compartilhar props:

  • Repetir as props manualmente nos subcomponentes, o que é pouco elegante.
  • Usar Context API, o que adiciona mais complexidade e código do que o necessário em muitos casos.

Se o Root recebe as props e envolve os subcomponentes, faz sentido que eles também tenham acesso a essas props de forma prática.


🤔 “Mas por que não passar as props direto nos filhos?”

Boa pergunta.
Em alguns casos, faz todo sentido — por exemplo, quando o Root não usa essas props.
Mas pense em situações em que o Root também precisa delas.

Um caso clássico é a prop id.
Todos os elementos React aceitam id, e ela costuma ser útil em testes.
Seria ótimo poder passar um único id para o Root e fazer com que cada subcomponente tivesse um ID derivado automaticamente, como:

<Root id="user">
  <Root.Header />   // id="user-header"
  <Root.Body />     // id="user-body"
  <Root.Footer />   // id="user-footer"
</Root>
Enter fullscreen mode Exit fullscreen mode

Esse mesmo raciocínio vale para className, disabled, atributos data-*, entre outros.

Sim, o Context resolveria.
Mas ele adiciona boilerplate, complexidade e até listeners internos de pub/sub — algo desnecessário quando queremos apenas espelhar props.


🧠 A solução: useEnhanceChildren

Apresento o useEnhanceChildren, um custom hook que facilita a injeção de props do componente pai nos filhos.
Ele deve ser utilizado dentro do componente Root, e conta com dois modos de operação:

1. Modo broadcast

No modo broadcast, o hook injeta o mesmo conjunto de props em todos os filhos, de forma uniforme (exceto elementos HTML nativos como <div> e <span>).

const children = useEnhanceChildren(props.children, {
  props: { disabled: true, className: 'root-child' }
});
Enter fullscreen mode Exit fullscreen mode

2. Modo map

No modo map, você especifica quais props vão para quais filhos, utilizando o displayName de cada subcomponente.

const children = useEnhanceChildren(props.children, {
  mapProps: {
    Header: { color: 'blue' },
    Footer: { color: 'gray' },
  }
});
Enter fullscreen mode Exit fullscreen mode

🔑 O displayName é essencial nesse modo.
É por meio dele que o hook identifica qual subcomponente deve receber quais props:

function Header(props: { color?: string }) { /* ... */ }
Header.displayName = 'Header';
Enter fullscreen mode Exit fullscreen mode

🧩 Type-safety com generics

Você pode definir um generic para garantir que os nomes e as props estejam corretos, evitando erros de digitação e garantindo a inferência de tipos adequada.

type MapProps = {
  'Card.Header': { title?: string };
  'Card.Footer': { description?: string };
};
const enhanced = useEnhanceChildren<MapProps>(children, {
  mapProps: {
    'Card.Header': { title: 'Título' },
    'Card.Footer': { description: 'Descrição' },
      // Erro de tipagem se você tentar algo errado, por exemplo:
      // 'Body': { wrongProp: true } ❌
    },
  });
Enter fullscreen mode Exit fullscreen mode

🧱 Exemplo completo

Para visualizar o useEnhanceChildren em ação dentro de um compound component real, veja o exemplo abaixo onde o componente Card injeta suas props (title, description) diretamente nos filhos via useEnhanceChildren — sem Context, sem prop drilling manual.

import type { ReactNode, ComponentPropsWithoutRef } from 'react';
import { useEnhanceChildren } from '@/hooks/useEnhanceChildren';

type CardProps = {
  title: string;
  description: string;
  children: ReactNode; // children será automaticamente omitido pelo hook
};

/*
type CardMapProps = {
  'Card.Header': { title: string };
  'Card.Footer': { description: string };
};
*/

export function Card({ children, ...props }: CardProps) {
  const enhancedChildren = useEnhanceChildren<CardProps>(children, {
    props, // modo broadcast
  });

  /*
  const enhancedChildren = useEnhanceChildren(children, {
    mapProps: { // modo map
      'Card.Header': { title: props.title },
      'Card.Footer': { description: props.description },
    }, 
  });
  */

  return (
    <div>
      {enhancedChildren}
    </div>
  );
}

// ----------------------
// Card.Header
// ----------------------
type CardHeaderProps = ComponentPropsWithoutRef<'header'> & {
  title?: string;
};

Card.Header = Object.assign(
  ({ title, ...rest }: CardHeaderProps) => (
    <header {...rest}>
      {title}
    </header>
  ),
  { displayName: 'Card.Header' },
);

// ----------------------
// Card.Body
// ----------------------
type CardBodyProps = ComponentPropsWithoutRef<'main'>;

Card.Body = Object.assign(
  ({ children, ...rest }: CardBodyProps) => (
    <main {...rest}>
      {children}
    </main>
  ),
  { displayName: 'Card.Body' },
);

// ----------------------
// Card.Footer
// ----------------------
type CardFooterProps = ComponentPropsWithoutRef<'footer'> & {
  description?: string;
};

Card.Footer = Object.assign(
  ({ description, ...rest }: CardFooterProps) => (
    <footer {...rest}>
      {description}
    </footer>
  ),
  { displayName: 'Card.Footer' },
);
Enter fullscreen mode Exit fullscreen mode

E seu uso na prática ficaria assim:

<Card title="Título principal" description="Descrição resumida">
  <Card.Header />
  <Card.Body>
    <p>Conteúdo interno do card.</p>
  </Card.Body>
  <Card.Footer />
</Card>
Enter fullscreen mode Exit fullscreen mode

Recursos e comportamento

✅ Recursividade: percorre hierarquias aninhadas — netos, bisnetos e tataranetos também recebem as props.
✅ Precedência correta: props passadas diretamente nos filhos têm prioridade (não são sobrescritas).
✅ Ignora elementos HTML nativos.
✅ Suporte a TypeScript forte: overloads e união discriminada para os modos map e broadcast.
✅ Memoização interna: evita re-renderizações desnecessárias.


Conclusão

O useEnhanceChildren nasceu da necessidade de simplificar algo bem específico: compartilhar props do Root com seus filhos em compound components — sem recorrer à complexidade da Context API.

Mas, embora tenha sido pensado para esse cenário, acredito que ele pode ser útil em outros contextos onde há herança natural de props. E, claro, ainda há espaço para evoluir — tanto em recursos quanto em ergonomia.

O hook não substitui o Context, mas preenche com elegância o espaço entre o prop drilling manual e o context overkill.

Se você trabalha com compound components, recomendo experimentar — e, por que não, contribuir com melhorias.
💬 Dúvidas, críticas e sugestões também são muito bem-vindas.

🔗 Veja o projeto completo no GitHub →

Top comments (0)