DEV Community

Cover image for Polymorphic Components: Authoring flexible and reusable components.
Paul-Simon Emechebe
Paul-Simon Emechebe

Posted on

Polymorphic Components: Authoring flexible and reusable components.

When building large react projects, building components to accommodate different variants and elements can be really hard, especially when you're trying to find a way to build reusable and flexible ones.

In this article I'll demonstrate how to use Polymorphic Components. It's a pattern that provides component consumers with flexibility to configure the components behaviors via props.
 
 

DISCLAIMER: this example is done using Next.js and tailwind. Nevertheless, this demonstration can be used on any other React framework like CRA.

 
 

if you are following using Next.js:


npx create-next-app@latest --typescript polymorphic-components with-tailwindcss

Enter fullscreen mode Exit fullscreen mode

if you are following using CRA


npx create-react-app polymorphic-components --template tailwindcss-typescript

Enter fullscreen mode Exit fullscreen mode

 
 

Configurations and dependencies

 

  • clsx- this is a package we have to install, a utility for constructing className strings conditionally.

$ npm install --save clsx

Enter fullscreen mode Exit fullscreen mode

for more information on clsx https://www.npmjs.com/package/clsx
 

 
 
 
We'll build a single Card Component that we'll reuse three times to display three cards. We'll configure the content/behavior of each of the cards via props in parent component which in this case is the Index component.

Image of the final look

File Structure

src
|--components
  |--icons
     |--ReactIcon.tsx
     |--SvelteIcon.tsx
     |--VueIcon.tsx
  |--polymorphic
     |--card.type.ts
     |--Card.ts

  |--Card.module.css
|--Index.tsx
 
 

src/components/polymorphic/card.type.ts

The Card component will support three variants - react , vue and svelte , so let’s create a file with the CardVariant type.

export type CardVariant = 'react' | 'vue' | 'svelte'
Enter fullscreen mode Exit fullscreen mode

 

src/components/Card.module.css
Here we give styles to each variant

.card {
  @apply text-left flex relative border-l-4;

  &.react {
    @apply flex flex-col bg-[#61dafb] bg-opacity-30 p-4 w-[300px] h-[390px] border-2;
  }
  &.vue {
    @apply flex flex-col bg-[#4dba87] bg-opacity-30 p-4 w-[300px] h-[390px] border-2;
  }
  &.svelte {
    @apply flex flex-col bg-[#ff3e00] bg-opacity-30 p-4 w-[300px] h-[390px] border-2;
  }

}
Enter fullscreen mode Exit fullscreen mode

 

let's create the icons!

 

src/components/icons/ReactIcon


type ReactIconProps = {}

const ReactIcon = (props: ReactIconProps) => {
  return (
    <>
    <svg height="150" viewBox="175.7 78 490.6 436.9" width="150" xmlns="http://www.w3.org/2000/svg">
      <g fill="#61dafb"><path d="m666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9v-22.3c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6v-22.3c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zm-101.4 106.7c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24s9.5 15.8 14.4 23.4zm73.9-208.1c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6s22.9-35.6 58.3-50.6c8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zm53.8 142.9c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6z"/><circle cx="420.9" cy="296.5" r="45.7"/></g>
    </svg>
    </>
  )
}

export default ReactIcon

Enter fullscreen mode Exit fullscreen mode

 

src/components/icons/VueIcon


type VueIconProps = {}

const VueIcon = (props: VueIconProps) => {
  return (
    <>
    <svg className="w-[150px]" enable-background="new 0 0 2500 2165.1" viewBox="0 0 2500 2165.1" xmlns="http://www.w3.org/2000/svg"><path d="m1538.7 0-288.7 500-288.7-500h-961.3l1250 2165.1 1250-2165.1z" fill="#4dba87"/>
      <path d="m1538.7 0-288.7 500-288.7-500h-461.3l750 1299 750-1299z" fill="#435466"/>
    </svg>
    </>
  )
}

export default VueIcon

Enter fullscreen mode Exit fullscreen mode

 

src/components/icons/SvelteIcon


type SvelteIconProps = {}

const SvelteIcon = (props: SvelteIconProps) => {
  return (
    <>
    <svg height="150" viewBox="-23.04085003 -23.7 545.4320132 647" width="150" xmlns="http://www.w3.org/2000/svg">
      <path d="m466.95 79.52c-55.66-79.62-165.6-103.22-245.08-52.6l-139.58 88.93c-9.39 5.9-18.15 12.76-26.12 20.47-7.98 7.71-15.13 16.23-21.34 25.42s-11.45 19-15.64 29.27a160.478 160.478 0 0 0 -9.26 31.87c-1.65 9.15-2.55 18.43-2.67 27.73-.13 9.31.52 18.61 1.93 27.8 1.41 9.2 3.58 18.27 6.48 27.11s6.53 17.42 10.85 25.66a161.68 161.68 0 0 0 -8.22 13.97c-2.51 4.79-4.77 9.71-6.78 14.73s-3.76 10.14-5.25 15.34-2.71 10.47-3.67 15.79a170.365 170.365 0 0 0 1.55 67.48c2.5 11.05 6.09 21.83 10.73 32.17s10.29 20.2 16.89 29.42c55.66 79.62 165.59 103.22 245.07 52.6l139.58-88.56c9.39-5.91 18.13-12.78 26.1-20.5a160.58 160.58 0 0 0 21.33-25.42c6.21-9.18 11.45-18.99 15.64-29.26 4.19-10.26 7.3-20.94 9.29-31.85 1.65-9.15 2.54-18.42 2.66-27.72s-.53-18.6-1.95-27.79c-1.41-9.19-3.58-18.25-6.49-27.09-2.91-8.83-6.54-17.41-10.86-25.65 2.97-4.51 5.72-9.18 8.23-13.97 2.5-4.79 4.77-9.71 6.78-14.73s3.77-10.14 5.27-15.34c1.49-5.19 2.73-10.46 3.7-15.78 1.98-11.16 2.84-22.49 2.58-33.82s-1.65-22.6-4.15-33.66c-2.5-11.05-6.09-21.83-10.73-32.17a170.906 170.906 0 0 0 -16.87-29.42" fill="#ff3e00"/><path d="m208.23 527.78a110.876 110.876 0 0 1 -33.49 3.42c-11.27-.58-22.39-2.86-32.97-6.79a111.06 111.06 0 0 1 -29.42-16.35 111.108 111.108 0 0 1 -23.15-24.42c-3.97-5.55-7.37-11.47-10.15-17.69a102.38 102.38 0 0 1 -6.45-19.34c-1.49-6.65-2.33-13.43-2.48-20.24s.38-13.62 1.58-20.33c.19-1.09.41-2.18.65-3.26.23-1.09.49-2.17.77-3.24.27-1.08.57-2.15.89-3.22.31-1.06.65-2.12 1-3.17l2.63-8.03 7.17 5.35c4.11 3 8.35 5.83 12.7 8.47 4.35 2.65 8.81 5.11 13.37 7.37 4.55 2.27 9.21 4.35 13.94 6.22 4.73 1.88 9.54 3.55 14.42 5.02l5.35 1.55-.48 5.35a31.395 31.395 0 0 0 1.12 10.81c.49 1.76 1.14 3.46 1.93 5.1s1.72 3.21 2.78 4.69a33.4 33.4 0 0 0 6.99 7.35c2.68 2.08 5.67 3.74 8.86 4.92s6.53 1.86 9.93 2.03c3.39.18 6.79-.17 10.08-1.03.76-.2 1.5-.43 2.24-.69s1.47-.54 2.18-.86c.72-.31 1.42-.65 2.12-1.02.69-.36 1.36-.75 2.02-1.17l139.37-88.94a28.96 28.96 0 0 0 4.75-3.72c1.45-1.41 2.74-2.96 3.87-4.63s2.07-3.46 2.83-5.33c.75-1.87 1.31-3.81 1.67-5.79.35-2.03.5-4.08.45-6.14-.05-2.05-.31-4.09-.77-6.1-.45-2-1.11-3.95-1.96-5.83-.84-1.87-1.88-3.65-3.08-5.32-1.94-2.79-4.29-5.26-6.98-7.34s-5.68-3.74-8.86-4.92a33.464 33.464 0 0 0 -9.93-2.04c-3.4-.17-6.8.18-10.09 1.03-.75.2-1.5.43-2.24.69s-1.46.54-2.18.85c-.72.32-1.42.66-2.11 1.03-.69.36-1.37.76-2.03 1.18l-53.52 33.98c-2.18 1.38-4.42 2.68-6.7 3.9-2.29 1.21-4.61 2.34-6.98 3.38s-4.78 1.99-7.22 2.84c-2.44.86-4.91 1.62-7.41 2.29-10.91 2.82-22.18 3.96-33.43 3.38s-22.34-2.87-32.9-6.78c-10.56-3.92-20.46-9.43-29.36-16.33s-16.7-15.11-23.13-24.36c-3.95-5.55-7.34-11.48-10.11-17.7-2.78-6.22-4.93-12.7-6.42-19.34-1.49-6.65-2.31-13.43-2.45-20.24-.15-6.8.38-13.61 1.59-20.31a96.419 96.419 0 0 1 14.94-36.86 96.283 96.283 0 0 1 28.57-27.68l139.8-88.93c2.17-1.38 4.39-2.68 6.66-3.9 2.27-1.21 4.59-2.34 6.94-3.38a98.21 98.21 0 0 1 7.18-2.84c2.42-.86 4.88-1.63 7.37-2.3 10.92-2.83 22.21-3.99 33.47-3.42 11.27.58 22.38 2.86 32.96 6.79 10.58 3.92 20.49 9.44 29.41 16.35a111.11 111.11 0 0 1 23.14 24.43c3.96 5.54 7.37 11.46 10.16 17.68s4.95 12.69 6.46 19.34c1.5 6.65 2.34 13.43 2.49 20.24.16 6.81-.36 13.62-1.56 20.33-.21 1.1-.43 2.2-.68 3.29-.24 1.09-.5 2.18-.78 3.26-.27 1.09-.57 2.17-.88 3.24-.31 1.08-.63 2.15-.98 3.21l-2.67 8.03-7.12-5.35c-4.12-3.03-8.37-5.87-12.73-8.54-4.36-2.66-8.84-5.14-13.41-7.43a182.39 182.39 0 0 0 -28.45-11.32l-5.36-1.55.49-5.35c.15-1.83.14-3.67-.03-5.49-.16-1.82-.49-3.63-.97-5.4-.49-1.76-1.12-3.49-1.91-5.14-.78-1.66-1.71-3.24-2.77-4.74a33.153 33.153 0 0 0 -6.99-7.2 32.991 32.991 0 0 0 -8.82-4.8 33.244 33.244 0 0 0 -19.83-.89c-.76.2-1.51.43-2.24.68-.74.26-1.47.55-2.19.86-.71.31-1.42.66-2.11 1.02-.69.37-1.37.76-2.03 1.18l-139.63 88.78c-1.7 1.07-3.29 2.32-4.73 3.72s-2.74 2.95-3.87 4.61a29.724 29.724 0 0 0 -2.83 5.31c-.76 1.87-1.32 3.8-1.68 5.78-.35 2.03-.5 4.09-.45 6.15a31.547 31.547 0 0 0 2.73 11.95 31.84 31.84 0 0 0 3.07 5.34c1.93 2.76 4.27 5.22 6.94 7.28a33.26 33.26 0 0 0 8.79 4.9 33.533 33.533 0 0 0 19.86 1.09c.75-.21 1.5-.44 2.24-.7.73-.26 1.46-.55 2.18-.86a29.2 29.2 0 0 0 2.11-1.02c.69-.36 1.37-.75 2.03-1.17l53.52-33.92c2.19-1.4 4.42-2.72 6.71-3.94 2.28-1.23 4.61-2.36 6.99-3.41a99.39 99.39 0 0 1 7.23-2.84c2.45-.86 4.93-1.62 7.44-2.28 10.92-2.84 22.2-4 33.47-3.44 11.27.57 22.38 2.85 32.96 6.77 10.57 3.92 20.49 9.43 29.4 16.35 8.92 6.91 16.72 15.14 23.15 24.41 3.96 5.55 7.36 11.47 10.15 17.69a102.65 102.65 0 0 1 6.46 19.34c1.5 6.64 2.34 13.42 2.5 20.23.16 6.82-.37 13.63-1.56 20.33a96.419 96.419 0 0 1 -5.55 19.21 95.753 95.753 0 0 1 -9.4 17.65c-3.73 5.54-8.03 10.68-12.83 15.33s-10.07 8.79-15.73 12.35l-139.64 88.93c-2.19 1.39-4.43 2.7-6.71 3.92-2.29 1.22-4.62 2.35-7 3.39-2.37 1.05-4.78 2-7.23 2.86-2.44.86-4.92 1.63-7.42 2.3" fill="#fff"/>
    </svg>
    </>
  )
}

export default SvelteIcon
Enter fullscreen mode Exit fullscreen mode

 

Now it's time to create the Card component

src/components/polymorphic/Card.tsx

import clsx from 'clsx'
import styles from '../../components/Card.module.css'
import { VueIcon, ReactIcon, SvelteIcon } from '../icons'
import { CardVariant } from "./card.types";

type CardProps = {
  show: boolean;
  showIcon?: boolean;
  variant: CardVariant;
  headerText: string;
  text?: string;
  children?: React.ReactNode;
};

const ICONS = {
  react: ReactIcon,
  svelte: SvelteIcon,
  vue: VueIcon,
};

const Card = (props: CardProps) => {
  const { children, show, text, headerText, variant, showIcon = true } =
    props;

  const Icon = ICONS[variant];

  return show ? (
    <div 
      className={clsx(styles.card, styles[variant])}
    >

      {/* Logo Icon */}
      {showIcon ? (
        <div className="h-1/2 flex justify-center items-center w-[100%]">
          <Icon />
        </div>
      ) : null}

      {/* Header text */}
      <div className="py-3 h-1/2 w-[100%]">
        {headerText ? (
          <div>
            <h1 className="text-2xl mb-1">{headerText}</h1>
          </div>
        ) : null}

        {/* Body text */}
        <div>
          <p className="text-sm">
            {text ? text : children}
          </p>
        </div>
      </div>
    </div>
  ) : null;
};

export default Card;

Enter fullscreen mode Exit fullscreen mode

Let's go through what is happening in the Card component
 
We make the Card component to accept props to give us the ability to configure what the Card should render

• show - indicates if the Card should be rendered
• variant - specifies the styling variant.
• showIcon - indicates whether the alert icon should be displayed. By default it’s set to true .
• headerText - text for the Card header.
• text - main body text for the Card.
• children - any HTML/React elements, components, etc.

type CardProps = {
  show: boolean;
  showIcon?: boolean;
  variant: CardVariant;
  headerText: string;
  text?: string;
  children?: React.ReactNode;
};

Enter fullscreen mode Exit fullscreen mode

 
Next, we have the ICONS variable, which maps variants to appropriate icon components.

const ICONS = {
  react: ReactIcon,
  svelte: SvelteIcon,
  vue: VueIcon,
};

Enter fullscreen mode Exit fullscreen mode

 
Here we destructure the props. Also we set the showIcon to true by default.

const { children, show, text, headerText, variant, showIcon = true } = props;
Enter fullscreen mode Exit fullscreen mode

 

Next, we create the Icon component to render the Icon according the variant prop.

const Icon = ICONS[variant];
Enter fullscreen mode Exit fullscreen mode

 
The Card component renders

  • an icon
  • a header text
  • the body
return show ? (
    <div 
      className={clsx(styles.card, styles[variant])}
    >

      {/* Logo Icon */}
      {showIcon ? (
        <div className="h-1/2 flex justify-center items-center w-[100%]">
          <Icon />
        </div>
      ) : null}

      {/* Header text */}
      <div className="py-3 h-1/2 w-[100%]">
        {headerText ? (
          <div>
            <h1 className="text-2xl mb-1">{headerText}</h1>
          </div>
        ) : null}

        {/* Body text */}
        <div>
          <p className="text-sm">
            {text ? text : children}
          </p>
        </div>
      </div>
    </div>
  ) : null;
Enter fullscreen mode Exit fullscreen mode

 

TIP: conditionally rendering your elements based on props ( especially when the prop is a user input ) is good for error handling. In the code above i render the element otherwise return null which won't render anything.

 

Let's use the Card component

src/Index.tsx

As said earlier, We will have three cards, one for each variant. Last but not least, we need to update the Index Component.

import Card from './components/polymorphic/Card'

const Home = () => {
  return (
    <div className='flex flex-col justify-center items-center min-w-[900px] mx-auto p-11'>
      <div><h1 className='text-3xl'>Polymorphic Components</h1></div>
      <div><h1 className='text-xs mb-5 mt-3'>by Paul-Simon Emechebe   tw:@ptbthefirst</h1></div>
      <div className='p-6 flex flex-row space-x-6'>
        <Card 
          show
          variant='react'
          headerText='React'
          text='React is a free and open-source front-end JavaScript library for building user interfaces based on UI components.'
        />
        <Card 
          show
          variant='svelte'
          headerText='Svelte'
          text='Svelte is a free and open-source front end compiler created by Rich Harris and maintained by the Svelte core team members.'
        />
        <Card 
          show
          variant='vue'
          headerText='Vue'
          text='Vue.js is an open-source model–view–viewmodel front end JavaScript framework for building user interfaces and single-page applications.'
        />
      </div>
    </div>
  )
}

export default Home

Enter fullscreen mode Exit fullscreen mode

 
 
💥💥💥 We just build a working Card component that can support different variants/elements. What are your thoughts? Can this pattern be applied to large applications? Have you tried something like this? I would love love to hear from you.

Top comments (4)

Collapse
 
fjones profile image
FJones

This is exactly counter to react paradigms (and only sort of "polymorphism").

The "react way" would be to write a Card component that accepts an icon and a className, and then having e.g. a ReactCard that renders Card with preset values for the icon and className. Coupling the Card directly with its supported variants actually hugely limits the reusability and flexibility, and requires that the Card be constantly kept up to date with all possible variants.

Collapse
 
paul_emechebe profile image
Paul-Simon Emechebe

Okay so what I'm understanding from this is that

  1. Create components dedicated to a particular variant e.g ReactCard.
  2. Make those components render another component called Card, where the Card component accepts an icon and a className. Right?
Collapse
 
fjones profile image
FJones

Precisely. I've made a few adjustments to make it work on codepen, but the gist is there:

Rather than selecting the variant settings from within the Card, we supply the variant settings to the card. The result is ultimately still that you just call, e.g. <ReactCard /> with your show, headerText, and text props, but now you can easily extend this to support, for instance, an AngularCard without having to touch Card at all:

Collapse
 
celine_emechebe_e652deb85 profile image
Celine Emechebe

This totally changed my life bro! Big W