Building blocks for strongly typed polymorphic components in React
react-polymorphic-box
Building blocks for strongly typed polymorphic components in React.
? Motivation
Popularized by Styled Components v4, the as
prop allows changing the HTML tag rendered by a component, e.g.:
import { Box } from 'react-polymorphic-box';
import { Link } from 'react-router-dom';
<Box as="a" href="https://github.com/kripod">GitHub</Box>
<Box as={Link} to="/about">About</Box>
While this pattern has been encouraged by several libraries, typings had lacked support for polymorphism, missing benefits like:
- Automatic code completion, based on the value of the
as
prop - Static type checking against the associated component's inferred props
- HTML element name validation
? Usage
A Heading
component can demonstrate the effectiveness of polymorphism:
<Heading color="rebeccapurple">Heading</Heading>
<Heading as="h3">Subheading</Heading>
Custom components like the previous one may utilize the package as shown below.
import React from 'react';
import { Box, PolymorphicComponentProps } from 'react-polymorphic-box';
// Component-specific props should be specified separately
export interface HeadingOwnProps {
color?: string;
}
// Merge own props with others inherited from the underlying element type
export type HeadingProps<
E extends React.ElementType
> = PolymorphicComponentProps<E, HeadingOwnProps>;
// An HTML tag or a different React component can be rendered by default
const defaultElement = 'h2';
export function Heading<E extends React.ElementType = typeof defaultElement>({
color,
style,
...restProps
}: HeadingProps<E>): JSX.Element {
// The `as` prop may be overridden by the passed props
return <Box as={defaultElement} style={{ color, ...style }} {...restProps} />;
}
Forwarding Refs
Library authors should consider encapsulating reusable components, passing a ref through each of them:
import React from 'react';
import { Box } from 'react-polymorphic-box';
export const Heading = React.forwardRef(
<E extends React.ElementType = typeof defaultElement>(
{ ref, color, style, ...restProps }: HeadingProps<E>,
innerRef: typeof ref,
) => {
return (
<Box
ref={innerRef}
as={defaultElement}
style={{ color, ...style }}
{...restProps}
/>
);
},
) as <E extends React.ElementType = typeof defaultElement>(
props: HeadingProps<E>,
) => JSX.Element;