DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Component Polymorphism in React
Anurag for ByteSlash

Posted on • Originally published at blog.anurag.tech

Component Polymorphism in React

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.

Top comments (4)

Collapse
 
lukeshiru profile image
Luke Shiru • Edited on

I recently wrote a comment in a similar post about how you should try to avoid this. Still, if you really really want to do this, TS can be improved a little:

import type { ComponentPropsWithoutRef } from "react";
import { createElement } from "react";

// This is better than `ElementType` for your use case
// Because it only allows tag names
export type TagName = keyof JSX.IntrinsicElements;

// This is more generic than `ButtonProps`
export type PolymorphicProps<Tag extends TagName> =
    ComponentPropsWithoutRef<Tag> & { readonly type?: Tag };

// And we can use it not just for button
export const Button = <Tag extends TagName>({
    type,
    ...props
}: PolymorphicProps<Tag>) => createElement(type ?? "button", props);

// But anything we want!
export const Container = <Tag extends TagName>({
    type,
    ...props
}: PolymorphicProps<Tag>) => createElement(type ?? "div", props);
Enter fullscreen mode Exit fullscreen mode

And then you use it like this:

<Button type="a" href="https://lshi.ru">Link</Button>
<Container type="section">Hi there!</Container>
Enter fullscreen mode Exit fullscreen mode

Again, you should avoid doing this. You might noticed already, but it's like doing React.createElement with extra steps, when ideally every component should render a specific html element.

Cheers!

Collapse
 
posandu profile image
Posandu

You always write comments. lol

Collapse
 
lukeshiru profile image
Luke Shiru

Just when the article is interesting enough for me or goes against the terms of DEV πŸ˜…

Collapse
 
fschwalm profile image
Fagner Schwalm • Edited on

Hello!
Thank you for sharing this very detailed article! I finally understood it.
One thing that i'm in doubt is how the as prop go to the generics T.
In my opinion to this solution works it would be something like:

<Button<a>  href="..." as="a"/>
Enter fullscreen mode Exit fullscreen mode

Could you explain it?

Thank you!

We are hiring! Do you want to be our Senior Platform Engineer? We're hiring for a Senior Platform Engineer and would love for you to apply.

Head here to learn more about who we're looking for.