A better way to build React component libraries
Published at 2022-07-05
Updated at 2022-07-05
Last update over 365 days ago
Licensed under MIT
react
javascript
web-development
Today we’ll quickly go over four programming patterns that apply to shared components in React.
Using these allows you to create a well-structured shared component library. The benefit you get is that developers in your organization can easily reuse components across numerous projects. You and your team will be more efficient.
Common Patterns
In this post, I show you four API patterns that you can use with all your shared components. These are:
- JSX children pass-through
- React
fowardRef
API - JSX prop-spreading cont TypeScript
- Opinionated
prop
defaults
Pattern 1: JSX Children Pass-Through
React provides the ability to compose elements using the children prop. The shared component design leans heavily on this concept.
Allowing consumers to provide the children
whenever possible makes it easier for them to provide custom content and other components. It also helps align component APIs with those of native elements.
Let’s say we have a Button
component to start with. Now we allow our Button
component to render its children
, like this:
// File: src/Button.tsx
export const Button: React.FC = ({ children }) => {
return <button>{children}</button>;
};
The definition of React.FC
already includes children
as a valid prop
. We pass it directly to the native button element.
Here is an example using Storybook to provide content to the Button.
// File: src/stories/Button.stories.tsx
const Template: Story = (args) => (
<Button {...args}>my button component</Button>
);
Pattern 2: forwardRef
API
Many components have a one-to-one mapping to an HTML element. To allow consumers to access that underlying element, we provide a referencing prop
using the React.forwardRef() API.
It is not necessary to provide a net
for day-to-day React development, but it is useful within shared component libraries. It allows for advanced functionality, such as positioning a tooltip relative to our Button
with a positioning library.
Our Button
component provides a single HTMLButtonElement (button)
. We provide a reference to it with forwardRef()
.
// File: src/buttons/Button.tsx
export const Button =
React.forwardRef <
HTMLButtonElement >
(({ children }, ref) => {
return <button ref={ref}>{children}</button>;
});
Button.displayName = "Button";
To help TypeScript consumers understand what element is returned from the ref
object, we provide a type
variable that represents the element we are passing it to, HTMLButtonElement
in this case.
Pattern 3: JSX Prop-Spreading
Another pattern that increases component flexibility is prop propagation. Prop propagation allows consumers to treat our shared components as drop-in replacements for their native counterparts during development.
Prop propagation helps with the following scenarios:
- Providing accessible
props
for certain content. - Adding custom data attributes for automated testing
- Using a native event that is not defined in our props.
Without prop propagation
, each of the above scenarios would require explicit attributes to be defined. prop propagation
helps ensure that our shared components remain as flexible as the native elements they use internally.
Let’s add prop propagation
to our Button component.
// File: src/buttons/Button.tsx
export const Button = React.forwardRef<
HTMLButtonElement,
React
.ComponentPropsWithoutRef<'button'>
>(({ children, ...props }, ref) => {
return (
<button ref={ref} {...props}>
{children}
</button>
);
});
We can reference our remaining props with the spread syntax and apply them to the button. React.ComponentPropsWithoutRef
is a type
utility that helps document valid props for a button element for our TypeScript consumers.
Some examples of this type checking in action:
// Pass - e is typed as
// `React.MouseEventMouseEvent>`
<Button onClick={(e) => { console.log(e) }} />
// Pass - aria-label is typed
// as `string | undefined`
<Button aria-label="My button" />
// Fail - type "input" is not
// assignable to `"button" |
// "submit" | "reset" | undefined`
<Button type="input" />
Pattern 4: Opinionated Defaults
For certain components, you may want to map default attributes to specific values. Whether to reduce bugs or improve the developer experience, providing a set of default values ​​is specific to an organization or team. If you find the need to default certain props, you should ensure that it is still possible for consumers to override those values ​​if necessary.
A common complexity encountered with button
elements is the default value type, "submit"
. This default type often accidentally submits surrounding forms and leads to difficult debugging scenarios. Here’s how we set the "button"
attribute by default.
Let’s update the Button
component to return a button with the updated type.
// File: src/buttons/Button.tsx
return (
<button ref={ref} type="button" {...props}>
{children}
</button>
);
By placing the default props before the prop broadcast, we ensure that any value provided by consumers is prioritized.
Look at some open source libraries
If you’re building a component library for your team, take a look at the most popular open source component libraries to see how they use the patterns above. Here’s a list of some of the top open source React component libraries to look into:
@khriztianmoreno
Until next time.