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

Top comments (0)