18
Component Polymorphism in React
What's Up, people! Hope you're doing fine!
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!
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
Here, our button will render as a
tag and it also accepts href
attribute.
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;
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";
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
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.
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;
Here, this code involves generics. The following line made this component generic:
const Button = <T extends ElementType = "button">
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:
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.
18