Gathering detailed insights and metrics for @axa-ch/react-polymorphic-types
Gathering detailed insights and metrics for @axa-ch/react-polymorphic-types
Gathering detailed insights and metrics for @axa-ch/react-polymorphic-types
Gathering detailed insights and metrics for @axa-ch/react-polymorphic-types
About Zero-runtime polymorphic component definitions for React
npm install @axa-ch/react-polymorphic-types
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
14 Stars
34 Commits
1 Forks
2 Watching
1 Branches
1 Contributors
Updated on 10 Oct 2024
TypeScript (99.31%)
JavaScript (0.69%)
Cumulative downloads
Total Downloads
Last day
4.5%
70
Compared to previous day
Last week
-42.5%
362
Compared to previous week
Last month
8.2%
2,210
Compared to previous month
Last year
596.2%
14,654
Compared to previous year
2
5
react-polymorphic-types is a library that enables the creation of zero-runtime polymorphic component definitions in React.
When building design systems or reusable UI components in React, you may come across the need for polymorphic components. A polymorphic component is a versatile component that can render different underlying HTML elements or custom components based on a prop.
A React polymorphic component provides flexibility to the consumer, allowing them to specify the desired element or component type to be rendered using a prop.
For example, let's consider a polymorphic heading component:
1import { createElement, ElementType, PropsWithChildren, ComponentProps } from 'react'; 2 3// Define the props for the polymorphic heading component 4type HeadingProps<T extends ElementType = 'h1'> = PropsWithChildren< 5 { 6 as?: T; 7 } & ComponentProps<T> 8>; 9 10// Define the polymorphic heading component 11export const Heading = <T extends ElementType = 'h1'>({ as = 'h1', children, ...rest }: HeadingProps<T>) => 12 createElement(as, rest, children);
In the above example, the Heading
component can render different heading levels (h1
, h2
, h3
, etc.) based on the as
prop. By default, it renders as an h1
element.
You can use the Heading
component in your application like this:
1const App = () => ( 2 <article> 3 <Heading>My Main Headline</Heading> 4 <Heading as='h2'>A Subtitle</Heading> 5 <p>A description</p> 6 </article> 7);
In this case, the same Heading
component is used to render two different semantic tags, h1
and h2
, allowing you to control the heading level and maintain consistency across your application.
Polymorphic components provide an elegant solution for building flexible and reusable UI components in React, enabling you to create a cohesive design system with consistent semantics.
The use of the as
attribute can become complex when adding constraints to your rendered markup or when using third-party components. Declaring polymorphic types for each component can also be a tedious task that you may want to abstract.
With @axa-ch/react-polymorphic-types
, you can easily add constraints to your polymorphic React components and avoid redundant type definitions.
Install the TypeScript types via npm:
1npm i @axa-ch/react-polymorphic-types -D
The following recipes provide a starting point for creating polymorphic components. You can copy and modify them according to your requirements.
This example showcases a simple polymorphic heading element. It allows you to independently define its size and markup using props.
1import { ComponentPropsWithoutRef, createElement, ElementType } from 'react'; 2import { PolymorphicProps } from '@axa-ch/react-polymorphic-types'; 3 4// Default HTML element if the "as" prop is not provided 5export const HeadingDefaultElement: ElementType = 'h1'; 6// List of allowed HTML elements that can be passed via the "as" prop 7export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 8export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6; 9 10// Component-specific props 11export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & { 12 size?: HeadingSizes; 13}; 14 15// Extend own props with others inherited from the underlying element type 16// Own props take precedence over the inherited ones 17export type HeadingProps<T extends HeadingAllowedElements = typeof HeadingDefaultElement> = PolymorphicProps< 18 HeadingOwnProps<T>, 19 T, 20 HeadingAllowedElements 21>; 22 23export const Heading = <T extends HeadingAllowedElements>({ 24 as = HeadingDefaultElement, 25 size, 26 className, 27 children, 28 ...rest 29}: HeadingProps<T>) => 30 createElement( 31 as, 32 { 33 ...rest, 34 className: `${className} size-${size || 1}`, 35 }, 36 children, 37 );
You can use the Heading
component in your application as shown below:
1const App = () => ( 2 <article> 3 <Heading 4 as='h1' 5 size={2} 6 > 7 My Main Headline 8 </Heading> 9 <Heading 10 as='h2' 11 size={5} 12 > 13 A Subtitle 14 </Heading> 15 16 {/* The following component will throw a TypeScript error because 'div' elements are not allowed here */} 17 <Heading 18 as='div' 19 size={5} 20 > 21 A Subtitle 22 </Heading> 23 <p>A description</p> 24 </article> 25);
This example is similar to the previous one, but it also allows the use of React refs.
1import { ComponentPropsWithoutRef, createElement, ElementType, forwardRef } from 'react'; 2import { PolymorphicProps, PolymorphicForwardedRef } from '@axa-ch/react-polymorphic-types'; 3 4// Default HTML element if the "as" prop is not provided 5export const HeadingDefaultElement: ElementType = 'h1'; 6// List of allowed HTML elements that can be passed via the "as" prop 7export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 8export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6; 9 10// Component-specific props 11export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & { 12 size?: HeadingSizes; 13}; 14 15// Extend own props with others inherited from the underlying element type 16// Own props take precedence over the inherited ones 17export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps< 18 HeadingOwnProps<T>, 19 T, 20 HeadingAllowedElements 21>; 22 23const HeadingInner = <T extends HeadingAllowedElements>( 24 { 25 as = HeadingDefaultElement, 26 size, 27 className, 28 children, 29 ...rest 30 }: PolymorphicProps<HeadingOwnProps<T>, T, HeadingAllowedElements>, 31 // notice the use of the PolymorphicForwardedRef type here 32 ref: PolymorphicForwardedRef<T>, 33) => 34 createElement( 35 element, 36 { 37 ...rest, 38 ref, 39 className: `${className} size-${size || 1}`, 40 }, 41 children, 42 ); 43 44// Forward refs with generics is tricky 45// see also https://fettblog.eu/typescript-react-generic-forward-refs/ 46export const Heading = forwardRef<HeadingAllowedElements>(HeadingInner) as unknown as < 47 T extends HeadingAllowedElements, 48>( 49 props: HeadingProps<T> & { ref?: PolymorphicForwardedRef<T> }, 50) => ReturnType<typeof HeadingInner>;
Using the @axa-ch/react-polymorphic-types
types will allow you to automatically infer the proper ref DOM node.
1const App = () => { 2 // The use of HTMLHeadingElement type is safe 3 const ref = useRef<HTMLHeadingElement | null>(null); 4 5 return ( 6 <Heading 7 ref={ref} 8 as='h2' 9 /> 10 ); 11};
This example shows the use of React.memo with a polymorphic component.
1import { ComponentPropsWithoutRef, createElement, ElementType, memo } from 'react'; 2import { PolymorphicProps } from '@axa-ch/react-polymorphic-types'; 3 4// Default HTML element if the "as" prop is not provided 5export const HeadingDefaultElement: ElementType = 'h1'; 6// List of allowed HTML elements that can be passed via the "as" prop 7export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 8export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6; 9 10// Component-specific props 11export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & { 12 size?: HeadingSizes; 13}; 14 15// Extend own props with others inherited from the underlying element type 16// Own props take precedence over the inherited ones 17export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps< 18 HeadingOwnProps<T>, 19 T, 20 HeadingAllowedElements 21>; 22 23const HeadingInner = <T extends HeadingAllowedElements>({ 24 as = HeadingDefaultElement, 25 size, 26 className, 27 children, 28 ...rest 29}: PolymorphicProps<HeadingOwnProps<T>, T, HeadingAllowedElements>) => 30 createElement( 31 element, 32 { 33 ...rest, 34 className: `${className} size-${size || 1}`, 35 }, 36 children, 37 ); 38 39// Memo with generics is tricky 40// see also https://fettblog.eu/typescript-react-generic-forward-refs/ 41export const Heading = memo(HeadingInner) as <T extends HeadingAllowedElements>( 42 props: HeadingProps<T>, 43) => ReturnType<typeof HeadingInner>;
The above component can be consumed without any additional overhead as follows:
1const App = () => ( 2 <> 3 <Heading as='h2' /> 4 </> 5);
Polymorphic exotic components allow you to use either DOM nodes or custom rendering functions for your HTML.
1import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent } from 'react'; 2import { PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types'; 3 4// Default HTML element if the "as" prop is not provided 5export const ContainerDefaultElement: ElementType = 'div'; 6// List of allowed HTML elements that can be passed via the "as" prop 7export type ContainerAllowedDOMElements = typeof ContainerDefaultElement | 'article' | 'section'; 8export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent; 9 10// Component-specific props 11export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>; 12 13// Extend own props with others inherited from the underlying element type 14// Own props take precedence over the inherited ones 15export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements 16 ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements> 17 : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>; 18 19export const Container = <T extends ContainerAllowedElements>({ 20 as = ContainerDefaultElement, 21 className, 22 children, 23 ...rest 24}: ContainerProps<T>) => 25 createElement( 26 element, 27 { 28 ...rest, 29 className, 30 }, 31 children, 32 );
The above component works with straight HTML nodes or with external exotic components like, for example, the ones provided by framer-motion.
1import { motion } from 'framer-motion'; 2 3const App = () => ( 4 <> 5 <Container as='div' /> 6 {/* Notice that the exotic props here will be automatically inferred */} 7 <Container 8 as={motion.article} 9 layout 10 /> 11 </> 12);
Polymorphic exotic components that use refs are slightly more complex and require some additional code to work properly.
1import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, forwardRef } from 'react'; 2import { PolymorphicProps, PolymorphicForwardedRef, PolymorphicExoticProps } from '@axa-ch/react-polymorphic-types'; 3 4// Default HTML element if the "as" prop is not provided 5export const ContainerDefaultElement: ElementType = 'div'; 6// List of allowed HTML elements that can be passed via the "as" prop 7export type ContainerAllowedDOMElements = 'div' | 'article' | 'section'; 8export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent; 9 10// Component-specific props 11export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>; 12 13// Extend own props with others inherited from the underlying element type 14// Own props take precedence over the inherited ones 15export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements 16 ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements> 17 : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>; 18 19// Forwarded ref component 20const ContainerInner = <T extends ContainerAllowedElements>( 21 { as = ContainerDefaultElement, className, children, ...rest }: ContainerProps<T>, 22 ref: PolymorphicForwardedRef<T>, 23) => 24 createElement( 25 element, 26 { 27 ...rest, 28 ref, 29 className, 30 }, 31 children, 32 ); 33 34// Forward refs with generics is tricky 35// see also https://fettblog.eu/typescript-react-generic-forward-refs/ 36export const Container = forwardRef<ContainerAllowedElements>(ContainerInner) as <T extends ContainerAllowedElements>( 37 props: ContainerProps<T> & { ref?: PolymorphicForwardedRef<T> }, 38) => ReturnType<typeof ContainerInner>;
With the above example, DOM nodes will be automatically inferred, including when using third-party exotic rendering functions.
1import { motion } from 'framer-motion'; 2 3const App = () => { 4 const div = useRef<HTMLDivElement | null>(null); 5 // Article and other HTML5 tags are just of type HTMLElement 6 const article = useRef<HTMLElement | null>(null); 7 8 return ( 9 <> 10 <Container 11 ref={div} 12 as='div' 13 /> 14 <Container 15 ref={article} 16 as={motion.article} 17 layout 18 /> 19 </> 20 ); 21};
This example combines multiple rendering strategies for your component to allow maximum flexibility for its consumers.
1// We need to infer the functional component properties so 'any' is used in this case 2// You can also add strict types for your functional components, but it will reduce flexibility 3import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, FC } from 'react'; 4import { PolymorphicFunctionalProps, PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types'; 5 6// Default HTML element if the "as" prop is not provided 7export const ContainerDefaultElement: ElementType = 'div'; 8// List of allowed HTML elements that can be passed via the "as" prop 9export type ContainerAllowedDOMElements = 'div' | 'article' | 'section'; 10export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent | FC<any>; 11 12// Component-specific props 13export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>; 14 15// Extend own props with others inherited from the underlying element type 16// Own props take precedence over the inherited ones 17export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements 18 ? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements> 19 : T extends FC<any> 20 ? PolymorphicFunctionalProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements> 21 : PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>; 22 23export const Container = <T extends ContainerAllowedElements>({ 24 as = ContainerDefaultElement, 25 className, 26 children, 27 ...rest 28}: ContainerProps<T>) => 29 createElement( 30 as, 31 { 32 ...rest, 33 className, 34 }, 35 children, 36 );
Let's see how we can use the above component with all its possible rendering options:
1import { motion } from 'framer-motion'; 2 3type FooProps = ComponentPropsWithoutRef<'div'> & { size: 'small' | 'large'; name: string }; 4 5const Foo: FC<FooProps> = ({ className, size = 'large', ...rest }) => ( 6 <div 7 {...rest} 8 className={`${className} the-foo ${size}`} 9 /> 10); 11 12const App = () => ( 13 <> 14 <Container as='div' /> 15 <Container 16 size='small' 17 name='foo' 18 as={Foo} 19 /> 20 <Container 21 as={motion.div} 22 layout 23 animate 24 /> 25 </> 26);
This project wouldn't exist without react-polymorphic-types
No vulnerabilities found.
No security vulnerabilities found.