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.