Component Polymorphism in React

Anurag - Jan 3 '22 - - Dev Community

What's Up, people! Hope you're doing fine!

hello

In this article, I'm going to explain Polymorphic Components in React, along with their implementation and using them with Typescript!

So, there is a high chance that you might not be familiar with this concept. But you may have encountered this pattern.

In a nutshell, this pattern lets us specify which HTML tag to use to render our component.

But, the flexibility of polymorphic components also makes them easy to misuse, and that’s where TypeScript can help us.

So, let's dive deep into this!

Overview - Polymorphic Components

First, let’s see how we would use polymorphic components in react. Say we have a Button component that we want to render as an HTML link. If Button is a polymorphic component, we can write it like this:

import Button from './Button'

function App() {
  return (
    <Button as="a" href="https://open.spotify.com">
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Here, our button will render as a tag and it also accepts href attribute.

Basic Implementation:

Note: To implement this in your react app, you need to have Typescript set up.

Now, let's implement a Basic example for this, without type checking for now:

const Button = ({ as, children, ...props }: any) => {
    const Component = as || "button";

    return <Component {...props}>{children}</Component>;
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

Here, we avoid type checking by setting the type to any.

Here, we render our component using the as prop or if it's not provided, use the button tag as fallback.

Here's the line which makes this work:

const Component = as || "button";
Enter fullscreen mode Exit fullscreen mode

That's all we need to build a basic implementation.

However, the problem with this approach is that there is no mechanism to prevent the client from passing incorrect props.

Here's an example:

import Button from './Button'

function App(){
  return (
    <Button href="https://open.spotify.com">
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Here, we are passing the href prop, which belongs to a tag, without setting the as prop to a.
Ideally, TypeScript would catch this bug immediately, and we would see an error.

Type checking using Typescript!

Next up, we are gonna tighten up the prop type using Typescript.

Here's a basic implementation for this:

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

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

const Button = <T extends ElementType = "button">({
  as,
  children,
  ...props
}: ButtonProps<T> & ComponentPropsWithoutRef<T>) => {
  const Component = as || "button";

  return <Component {...props}>{children}</Component>;
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

Here, this code involves generics. The following line made this component generic:

const Button = <T extends ElementType = "button">
Enter fullscreen mode Exit fullscreen mode

ElementType is a type from React. We set our parameter T to ElementType to ensure our button only accepts HTML tags and other React component types.

At this point, our Button component can dynamically calculate the props it accepts based on the value of as. If we try our client example earlier, we will see an error like this:

Screenshot (330).png

Here, we get an error stating that Property 'href' does not exist on type 'IntrinsicAttributes & MyButtonProps<"button">

That's it! Our Button component no longer accepts the href property because it doesn’t render itself as a link. If we add as="a", the error goes away.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .