The problem pattern
A common pattern for a Button is to do the following.MouseEvent<HTMLButtonElement>
, what if I wanted this button to be an anchor (<a>
) instead? The HTMLButtonElement
would not apply. Additionally, onClick
might not apply at all, we might just want the styling and pass in a href
.
We can do some interface tricks for this to work.
<Link>
component, however it is used to wrap an <a>
tag. If we want to use the passHref
prop, we’ll probably have to tweak the interface, since now, href
can be optional and it might have to work with forwardRef
, which needs to work with both <a>
and <button>
.
I think at this point we can agree this is probably not useful, and will cause a simple button, that is used throughout your project, to be insanely complex.
Breaking down the issue
So, lets ask ourselves, why are we making this so complex? We know that many other libraries withButton
components, allows you to switch the underlying DOM node. Many times they use the as
keyword because of styled components. So should we just use that library or styled components?
When I have seen this used, it has been in the context of keeping styling consistent and easily editable at a global level. Many times we have to alter the className
of a button based on the props. So if one of the main reasons is styling, why don’t we just abstract that out and have different components for a <a>
and <button>
styled buttons? Once we use a button in our application and decided on the element needed, what are the chances that it will need to change ever? Probably really low, like really low. So lets address the styling shall we.
Styling abstraction
Let’s start with our CSS, just going to keep this simple. We have a root class with different variations based on the props passed down.BaseButton
interface is staying. WOOOO. But not in the same way.
className
of the button? Easy, write a hook.
React Hooks don’t have to wrap other hooks (useState
for example), they can just be used to wrap functionality. We will also make use of a npm package called classnames
, this allows us to easily create className
strings. It isn’t required, just makes life easier.
Building the buttons
Now that we have this hook that will create our base class names, how do we apply it to our different button types. Pretty easily, instead of creating 1 component to rule them all, we create multiple components based on the use case. Before we show that though, I want to briefly talk about extending HTML attributes as props. When using typescript, you canextend
other interfaces which helps to create more generic interfaces and build up the complex interfaces. One really useful aspect here is the ability to extends HTML attributes based on the element.
<button>
element without needing to define them yourselves. Depending on your project, you might not want/need this flexibilty. However, for the examples below, I will be using them.
Button
Anchor
Anchor
is a <a>
and a Button
is a <button>
. Additionally, it will make other components in your application a lot more descriptive without needing inspect the props to determine what the component will render.
Additionally, just in these two components there were a very small changes that would have been, not annoying, but would add additional conditional into your base component to achieve which HTML element is rendered, what additional classes should be added etc.
You’ll also notice that our interfaces are really simple, the HTMLAttributes
interface really does the heavy lifting for us in this case, again, making these components feel more native, which is exactly what you want for a Block/Atom Component.
Usage
This sandbox shows the above code implemented to a relatively final state, the styling is a bit basic just to show off the different variants so that will need clean up and finalised. However, the React components have been written in the same way as above and everything seems to be working quite well.
One of the changes I did was to wrap the
Anchor
in a forwardRef
. This will allow us to use this custom component with the Next.js Link
component.
Button
at all, and existing Anchor
elements in the app will work perfectly fine as they did before. We were also able to type the ref object to be specifically a HTMLAnchorElement
instead of a HTMLAnchorElement|HTMLButtonElement
. Which again, adds in those conditional checks and also might behave weirdly with the <Link>
component.