With my present improvement workforce, I am constructing a reusable react element library. A few of these parts, like our <Button/>
, are pretty easy. I can simply expose all of the Button’s choices and functionalities via its props. One thing like this:
<Button
id="my-cool-button"
onClick={() => console.log("I obtained clicked!")}
variant="secondary"
disabled={false}
dimension="giant">
Press Me!
</Button>
However even for such a easy instance, we find yourself with fairly a couple of traces of code. This drawback of getting too many props will get way more dramatic after we start taking a look at advanced “composed-components” that are made up of many alternative items.
Take, for instance, the <Modal/>
. Our design consists of: a header with a number of configurations together with main title, subtitle, a hero picture, and/or a detailed button; a call-to-action footer permitting a variable variety of buttons in numerous layouts; and a main content material part. Along with the usual props of every Textual content, Picture, and Button element for all of these things, our shoppers have requested for the flexibility to do further structure and magnificence customization.
That is loads of props!
There needs to be a greater means to do that — and there’s! This weblog submit goals to cowl the answer that our workforce has taken in answering the next query:
How can we construct “composed-components” to offer default kinds, customizability, and a clear API?
And the best way to do you do all of it in TypeScript? 😉
The Downside
First, let’s dig a bit of deeper into precisely what we wish to accomplish.
To start out consuming our library, all a workforce has to do is yarn add "our-react-library"
and import { Element } from "our-react-library";
. Most builders will not ever have a look at our code; as a substitute, they will browse element documentation in our interactive Storybook. As we prioritize ease-of-use, we would like all of our parts to look nice out of the field.
Along with a React library, our workforce additionally publishes world design requirements and a design element library to be used throughout the corporate. Nevertheless, there are sometimes edge instances or conditions the place a workforce desires to make tweaks or modifications. These are simple sufficient to approve in design, however usually require us to reveal many layers of classNames (eww) or add much more props in React. Our resolution for the modal must help a number of variants, however keep away from introducing an ungainly variety of props.
Lastly, we at all times wish to present a terrific developer expertise — meaning delivering an API that permits our customers to write down clear, concise code. Utilizing lengthy element names like ModalPrimaryTitle
or polluting our bundle namespace with generic PrimaryTitle
aren’t acceptable options. Neither is utilizing nested objects as props to cover config or choices, which is tough to doc and does not work effectively with Storybook. And naturally, we wish to construct TypeScript-first 🦸♂️.
The Course of
I began this journey with our previous modal, which had loads of props and nonetheless was very difficult to customise. And, the brand new design our workforce got here up with included extra choices and suppleness than earlier than.
We knew very early on that we wished to keep away from any resolution that was too prop-heavy, which pushed us in direction of exposing a number of components to the person. One thing like this:
<Modal>
<h1>Foremost Title</h1>
<h2>Subtitle</h2>
{bodyContent}
<button>CTA 1</button>
</Modal>
One early suggestion was to replace our code-snippet generator in Storybook to return uncooked HTML that regarded like a modal, so we did not even want to make a element. However, that resolution would detach our shoppers code from our library, rendering us unable to push new options or fixes with out them updating their code. It might even be tough to model, as a result of we used styled-components as a substitute of counting on class names or bundling a stylesheet.
Nonetheless, we appreciated the path we have been headed in. The following suggestion was to offer a easy Modal that acted as a container, permitting customers to move our different present parts into it. However the structure was too advanced to sort out with out further wrappers for the header and footer, so we added these which gave us better customizability.
import {
Modal,
ModalHeader,
ModalContent,
ModalFooter,
Textual content,
Button
} from "our-react-library";
<Modal>
<ModalHeader>
<Textual content as={"h1"} dimension={6} weight={"daring"}>Foremost Title</Textual content>
<Textual content as={h2} dimension={4}>Subtitle</Subtitle>
<ModalHeader>
<ModalContent>
{bodyContent}
</ModalContent>
<ModalFooter>
<Button
variant={"main"}
dimension={"giant"}
onClick={() => console.log("Clicked!")}
/>
Go forth and prosper
</Button>
</ModalFooter>
</Modal>
That is wanting higher, but it surely nonetheless had a couple of points. Specifically, we have been (1) asking our customers to manually apply default kinds to the title, subtitle, name to motion buttons, and extra; and (2) we have been polluting our namespace with plenty of Modal-specific parts. The answer to the first drawback is straightforward, but it surely exasperates the second drawback: introduce a ModalTitle
element, a ModalSubtitle
element, a ModalCTA
element, and so on. Now if we are able to simply discover a easy place to place all these pesky parts, we’d have a reasonably good resolution!
What if we put the sub-components on Modal
itself?
The Answer
Beneath is the API we selected. Each element matches our design out of the field, but additionally permits for personalization utilizing CSS courses or styled-components. Including or eradicating full sections or including customized parts wherever within the stream is totally supported. The API is clear and concise and most significantly the namespace is immaculate.
import { Modal } from "our-react-library";
<Modal>
<Modal.Header>
<Modal.Title>Foremost Title</Modal.Title>
<Modal.Subtitle>Subtitle</Modal.Subtitle>
</Modal.Header>
<Modal.Content material>
That is my physique content material
</Modal.Content material>
<Modal.Footer>
<Modal.CTA>Click on me!</Modal.CTA>
</Modal.Footer>
</Modal>
Now I do know what you are considering, “That appears nice, however how are you going to make that work in TypeScript?”
I am glad you requested.
We use React practical parts to construct most of our library, so contained in the library, our recordsdata look one thing like this:
export const Button = ({ kids, dimension, ...props}: ButtonProps): JSX.Factor => {
if (dimension === "jumbo") {...}
return (
<StyledButton dimension={dimension} {...props}>
{kids}
</StyledButton>
);
}
TypeScript, nonetheless, doesn’t permit us to assign further props to a const, particularly after we export it. This poses an issue. One way or the other we have now to connect props to what’s basically a operate with out writing a ton of duplicate code. One other pesky drawback is setting appropriate displayNames for React DevTools and, extra importantly, our Storybook code generator.
This is the magic operate:
import React from 'react';
/**
* Attaches subcomponents to a guardian element to be used in
* composed parts. Instance:
*
* <Dad or mum>
* <Dad or mum.Title>abc</Dad or mum.Title>
* <Dad or mum.Physique prop1="foobar"/>
* </Dad or mum>
*
*
* This operate additionally units displayname on the guardian element
* and all kids element, and has the proper return kind
* for typescript.
*
* @param displayName topLevelComponent's displayName
* @param topLevelComponent the guardian ingredient of the composed element
* @param otherComponents an object of kid parts (keys are the names of the kid parts)
* @returns the highest degree element with otherComponents as static properties
*/
export operate attachSubComponents<
C extends React.ComponentType,
O extends File<string, React.ComponentType>
>(displayName: string, topLevelComponent: C, otherComponents: O): C & O {
topLevelComponent.displayName = displayName;
Object.values(otherComponents).forEach(
(element) =>
(element.displayName = `${displayName}.${element.displayName}`)
);
return Object.assign(topLevelComponent, otherComponents);
}
The above code lives in a util file and may simply be imported inside our library any time we wish to use sub-components. That permits us to write down a modal element file that may be very simple on the eyes:
export const Modal = attachSubComponents(
"Modal",
(props: ModalProps) => { ... },
{ Header, Content material, Footer, Title, Subtitle, HeroImage, ... }
);
And better of all, it is a terrific resolution for all of our customers!
Thanks for studying! I hope this method for creating clear composed parts in React will level-up you and your workforce.
Kaeden Wile
wile.xyz