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>
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>
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>
);
};
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>
);
}
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;
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>
);
}
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