Polymorphic React Components in TypeScript

Polymorphic React Components in TypeScript

Write flexible components

Reusable components are one of the key concepts in React — where you write a component once and get to reuse them multiple times.

Basically your component would receive some props, You then go ahead to use these internally and then finally render some React element which gets translated to the corresponding DOM element.

So what if my component can provide me the flexibility of controlling whatever the container element/node can be? This is what gave birth to the pattern known as Polymorphism.

What are Polymorphic component

By definition, the word Polymorphic means existing in more than one form. In the world of React components, a polymorphic component is a component that can be rendered with a different container element. With that said even though it is possible that you’ve never heard of a polymorphic component before. It’s a pattern for code reusability just like custom hooks, higher order component etc.

How Does Polymorphic component works

Let’s say we have a shareable <Box /> component and we want to decide what the actual element would be in 2 different cases,

  • We want to render the Box component as a anchor element with href attribute
  • We want the Box component to be a heading element of h1.

Now with the concept of polymorphism in React we can make this possible by adding an as props to the component to allow the caller have the flexibility of defining the HTML tag of their choice.

<Box as="a" href="https://www.linkedin.com/in/chisom-okoye-399112122/">
   Home page
</Box>

<Box as="h1">Main heading</Box>

Basic Implementation

Building your first polymorphic component is quite straight forward, here is a basic implementation:

export const Box =({as, children, ...others})=> {
   let Component = as || "span";

   return (
       <Component {...others}>{children}</Component>
   )
}

One thing to note here is that we default the as props to be span in case where the user isn't passing the props, it should work with span as the HTML element. And secondly we can't make use of the as props directly as React will think it is the name of an HTML tag (<as>) which is not. So we need to first assign it to a capitalize variable and then render it. Also the ...others is important because we spread it into <Component> as well. This allows us to pass along additional attributes or props that we don’t know about to the underlying element. An example is the href attribute needed for the <a> tag or the alt attribute used for an Image <img>.

That's all we need to build a basic polymorphic component.

However the problem with this approach is that there is no way or means to prevent the user from passing in unsupported props or passing in an invalid HTML element. i.e when we use this component as h1 the user might be tempted to pass along href attribute or the user would pass in any value for the as props which is not a valid HTML element.

Here comes the benefit of Typescript in React.

Leveraging Typescript to create a better polymorphic component.

To leverage typescript in making a bug free and better experience polymorphic component, you need to first ensure that the react application has typescript. To create a react typescript project you can use the command npx create-react-app project-name --template typescript or to add typescript to an existing react application, npm or yarn can be use npm install --save typescript @types/node @types/react @types/react-dom @types/jest. Then with the understanding of generic in typescript in we can tighten up the props being passed to the component.

Better Implementation

import { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";

type BoxProps<T extends ElementType > = {
  as?: T;
  children: ReactNode;
};

export const  Box =<T extends ElementType = "span">({
as, 
children, 
...others
}: BoxProps<T> & ComponentPropsWithoutRef<T>) => {
   let Component = as || "span";

   return (
       <Component {...others}>{children}</Component>
   )
}

What we are essentially doing here is that we create a type for the Box component which is

type BoxProps<T extends ElementType> = {
  as?: T;
  children: ReactNode;
};

Explanation:

Where T extends ElementType is a generic type, stating that T is a special variable type for the argument in our case the argument is as then the extends ElementType mean we are constraining the generic to be a valid HTML Element, i.e as can't be receiving values that's not a correct HTML element. It is also good to note that the ? operator attached to the as is to make it optional so typescript won't shout about this error when the component isn't receiving the as props. While the children props should be a ReactNode type

Then At the point of creating the Box component you can see that our generic type T was assigned to "span", meaning that the default element for as props would be span when the user isn't passing any. Then we utilize the BoxProps type we created to be the type for the argument we are expecting.

The next strange syntax from our Box Component is & ComponentPropsWithoutRef<T>. If you are coming from Styled-components this syntax won't be strange to you because it is actually the syntax format for styling pseudocode

 a {
      color : blue;
         &:hover{
            color: red;
        }
   }

So in Typescript we extract this format and introduce something known as intersection, meaning that the type BoxProps is an object type containing as, children and base on the type of as props we will return valid component props that correlate with the string attribute passed.

Conclusion

With that said if we go ahead to try out our <Box /> component wrongly e.g by passing a valid as props with the other incompatible props, you will get an error.

<Box as="h1" href="https://www.linkedin.com/in/chisom-okoye-399112122/">
    Main heading
</Box>

h1 is a perfect HTML element for the as props, but a h1 element should not have an href attribute. That's wrong and during the runtime typescript will help catch this error as: Property 'href' does not exist on type ...

This is great because we have gotten a better developer experience and even better robust solution.