What are Polymorphic components?
If you've ever tried any React component libraries like Material UI, Mantine UI or Chakra UI.
You might have used the as
prop, which is available in most of the library components,
the general idea behind this prop is to allow the consumers of your library to pass a
custom element or another component that can be rendered instead of a hard-coded value.
There are plenty of confusing definitions out there when it comes to this topic, though the way I like to think about it is this:-
A polymorphic component is a design pattern that allows a component to act in more than one way depending on the UI requirements without breaking any accessibility or Type checking. So a
Button
might work as a Navigation-link or vice versa.
A key thing to note here is that it's possible to build them without Typescript, however it is rudimentary and even incomplete without the type checking.
Let's see an example of a polymorphic Text
and Button
component.
At first glance, the Text
component looks super simple. All it takes is a tag name and
renders that element with the given content, so what gets rendered on the page is this
The Button
component is where things start to become a little tricky, and this is where we see the true power of polymorphism.
button
and a
both are semantically very different, eg. a button does not accept an
href
attribute whereas an a
tag does.
Now let's build one from scratch.
Project setup
You can use any frontend tooling like CRA, Snowpack or Vite
I will use Vite, to quickly create a new React + Typescript application with Vite I can do
Creating the Text component
Our Text
component should be able do the following
- Accept an
as
prop. - Type check props against attributes of the element passed in
as
prop. - Accept a
weight
prop to customise the font-weight of the element.
Here is how our basic Text
component would look like
if it's your first time using ComponentPropsWithoutRef
utility
type, no worries.
All is does is to take a React.ElementType
eg: span
and return an object type of all
the available attributes
for that element.
Cool, the basic component stuff is done but! it's still not Polymorphic.
Supporting the as
prop
We will check if the as prop is provided, if it is then render
that as the Component
otherwise default to a regular span
tag.
So now if we try it like this,
_3<Text as="a" href="http://sidwebworks">_3 Hello world_3</Text>
Typescript screams at us with this weird error. Let me simplify it for you.
All it's trying to say is href
does not exists on TextProps
which is quite right as
we passed span
to the ComponentPropsWithoutRef
utility and spans don't have a href
attribute.
Let's fix this.
Improving type support for as
prop
The problem we now have is, when the as
prop changes, the value passed to
ComponentPropsWithoutRef is still hard-coded.
So we need some way to pass a dynamic value in there which changes as we update our as
prop value.
Luckily for us Typescript offers a feature
called Generics
which can help us here.
A Generic is essentially a type value which can be passed around to other types and functions, kind of like variables which only Types can access.
So let's Genericify our component :)
In order to do that, it would be better to have a utility type like a
PolymorphicPropsWithoutRef
which takes 2 generic types C
(as prop) and P
(custom
props)
We're just making this utility so we can have some reusability in our code.
Writing a utility type
Umm so what the hell does this type even do? Let's break it down a bit.
In the above highlighted code, like the name says we create a type for the as
prop that takes in a generic C
which extends the ElementType
Extending other types like this is called as Generic constrains meaning it will only accept a value that satifies that given constrain.
Next, we create our main utility PolymorphicPropsWithoutRef
, which again takes in 2 generics C
for the ElementType and an optional type P
for custom props which we default to as an empty object.
In this type we make use of 3 other utility types and compose them together.
These utilities are :-
PropsWithChildren
: comes from React, adds thechildren
prop.AsProp
: we made this! it adds theas
prop.ComponentPropsWithoutRef
: comes from React, adds the element's HTML attributes.
Nice! let's use it back in our Text Component.
Works like a charm! Lets add our weight
prop.
Adding a custom size
prop
So we will have some default font weights object that our component will accept, and then use the keyof
operator to extract a union type from that object.
Then we can pass it to our polymorphic utility type
Next we can merge the style
object coming from the props and add an fontWeight
key, which will be the weight
prop's value
That's it
Now you can use these same techniques to create other polymorphic components like Button
, Icon
etc.