DEV Community

Adrian Knapp
Adrian Knapp

Posted on

React: O que é, e como funciona um Compound Component?

Recentemente encontrei um desafio no trabalho: A ideia seria criar um componente Stepper, onde os métodos do componente precisavam ser compartilhados, e acessados de forma externa. A questão é como, e qual a forma mais efetiva? É aí que os Compound Components entram (mas eu só fui saber disso após tentar algumas outras coisas).

A ideia é ter um ou mais componentes que trabalham juntos para atingir um objetivo. Normalmente, um componente é o pai, e os outros são os filhos. Com o intuito de prover uma API mais flexível e expressiva.

Algo como o <select> e <option>:

<select>
  <option value="option1">label1</option>
  <option value="option2">label2</option>
  <option value="option3">label3</option>
</select>
Enter fullscreen mode Exit fullscreen mode

Caso você tente usar um sem o outro, não irá funcionar. Agora vamos imaginar caso não tivéssemos uma API de compound components para trabalhar. (E lembre, isso é apenas HTML, não JSX).

<select options="option1:label1;option2:label2;option3:label3"></select>
Enter fullscreen mode Exit fullscreen mode

Claro que há algumas outras formas de imaginar isso, mas enfim. E como você expressaria o atributo disabled em uma API como essa? As coisas complicam.

Já os compound components, disponibilizam uma boa forma de conectar e interagir entre componentes.

Outro ponto importante sobre esse conceito de "estado implícito": O <select>, implicitamente armazena o estado da opção selecionada e compartilha isso com seus childrens, sendo renderizado da forma correta dependendo desse estado. Mas isso está implícito e não conseguimos ter acesso em nosso HTML.

Em minhas tentativas de tentar criar o componente Stepper, minha primeira abordagem foi através de Render Props, onde o componente <Stepper>, disponibiliza propriedades para o children, permitindo manipular os steps.

const Stepper = ({ children, initial = 0 }) => {
  const [active, setActive] = useState(initial);

  const nextStep = () => {
    if (active < children().props.children.length - 1);
      setActive(active + 1);
  };

  const prevStep = () => {
    if (active > 0) setActive(active - 1);
  };

  return (
    <div data-cid="stepper">
      {
        children({
          nextStep,
          prevStep,
          setActive,
          active
        }).props.children[active]
      }
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

E esse seria um exemplo de uso:

const steps = ["Step 1", "Step 2", "Step 3"];

const App = () => {
    return (
        <Stepper>
          {({ nextStep, prevStep, active }) => (
            <>
              {steps.map((step) => (
                <div key={step} id="step">
                  <p>{step}</p>
                  <button onClick={prevStep}>Prev Step</button>
                  <button onClick={nextStep}>Next Step</button>
                </div>
              ))}
            </>
          )}
        </Stepper>
    );
}
Enter fullscreen mode Exit fullscreen mode

Porém, ao pesquisar um pouco mais, encontrei alguns conteúdos, e o que mais me chamou atenção, foi um artigo do Kent C. Dodds , que explica e soluciona isso através da Context API do React, entregando os métodos via Hooks. Tornando esse o uso:

import React, {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState
} from "react";
import CommonStep from "../CommonStep";

const StepperContext = createContext(null);

type StepperContextModel = {
  nextStep: () => void;
  prevStep: () => void;
  setActive: (index: number) => void;
  active: number;
};

type StepperProps = {
  children: JSX.Element[];
};

const Stepper = ({ children }: StepperProps) => {
  const [active, setActive] = useState(0);

  const nextStep = useCallback(() => {
    if (active < children.length - 1) setActive(active + 1);
  }, [active, children]);

  const prevStep = useCallback(() => {
    if (active > 0) setActive(active - 1);
  }, [active]);

  const value: StepperContextModel = useMemo(
    () => ({
      nextStep,
      prevStep,
      setActive,
      active
    }),
    [active, nextStep, prevStep]
  );

  return (
    <StepperContext.Provider value={value}>
      {children[active]}
    </StepperContext.Provider>
  );
};

export const useStepperContext = (): StepperContextModel => {
  const context = useContext(StepperContext);

  if (!context) {
    throw new Error(
      `Stepper compound components cannot be rendered outside the Stepper component`
    );
  }

  return context;
};

const Step = ({ children }) => {
  const { nextStep, prevStep, active } = useStepperContext();

  return (
    <CommonStep nextStep={nextStep} prevStep={prevStep} active={active}>
      {children}
    </CommonStep>
  );
};

Stepper.Step = Step;

export default Stepper;
Enter fullscreen mode Exit fullscreen mode

E esse, um exemplo de uso:

const steps = ["Step 1", "Step 2", "Step 3"];

const App = () => {
    return (
        <Stepper>
            {steps.map((step) => (
                <Stepper.Step>
                    <p>{step}</p>
                </Stepper.Step>
            ))}
        </Stepper>
    );
}
Enter fullscreen mode Exit fullscreen mode

Exemplo dos componentes em ação: https://codesandbox.io/s/stepper-function-og2bl1
E o repositório: https://github.com/AdrianKnapp/compound-components

Basicamente, funciona por um contexto criado com o React, que armazena o estado e os mecanismos para o atualizarmos. Sendo o <Stepper>, o componente responsável por prover o valor desse contexto para o resto da árvore de elementos.

Muito mais simples, não? Porém, cada alternativa tem suas vantagens e casos de uso. Tendo utilidade em muitos cenários, não só em um Stepper, por exemplo.

Espero que esse conteúdo ajude você a solucionar futuros problemas, e que gere novas ideias na hora de criar componentes, criando APIs mais expressivas e efetivas.

Esse artigo foi baseado e inspirado em um post do Kent C. Dodds, feito em seu blog. Acesse em: https://kentcdodds.com/blog/compound-components-with-react-hooks

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

AWS GenAI LIVE!

GenAI LIVE! is a dynamic live-streamed show exploring how AWS and our partners are helping organizations unlock real value with generative AI.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️