If you'd rather read it in portuguese, I've made a pt-BR version of this post.
Basically everyone I know ends up creating reusable components when learning about the DRY concept. This is a deceptively simple abstraction: it's easy to create a Button, but it's surprisingly difficult to reuse it across all use cases in a project.
I've learned a few things over the years, and I see that a lot of people end up messing around without realizing it. Let me share some tips to avoid a messy code.
I have a React bias but everything can be applied in other contexts, as long as there is UI componentization.
Quick Access
- Avoid margins
- Based on HTML props
- Compose components
- Use headless libraries
- Avoid external dependencies
- Migration
- Putting it all together
1) Avoid margins
Placing margins in your component ends up breaking the expectation of component reuse. Spacing is important, and your component does not know the context where it will be applied.
Instead, leave the spacing responsibility to the parent containing the components. It is possible that it uses a different way to manage spacing, such as paddings and gaps.
There are several ideas ¹ ² ³ about how spacing should be created in components. But generally the conclusion is not to put spacing externally inerent to your component. It is more reliable for its parent component to handle margins.
And speaking of the parent component...
2) Based on HTML props
🔗
It is important that components are used in an expected way. Using an interface that everyone knows helps with this, and HTML attributes are a known interface.
If you want to insert a margin into the component, instead of creating a prop dedicated to the margin, your component can make it a generic div
and accept styles and classes as props. The parent component will have the context and control to extend its component for unexpected cases.
"But this pattern is very common in the project, should I repeat its declaration in the parent?" Some props can be added alongside the traditional props. It's usually a variant
prop, or size
or similar. I recommend taking a look at libs like CVA
or vanilla-extract
to manage these props in a scalable way.
If the prop has a standard HTML name, it also requires no documentation. For new devs it is easier to learn, and for experienced people they create expectations and in turn increases productivity.
And this tip includes not only the name of each prop but also the value. Avoid creating an innovative contract for the prop, such as enums and objects. Instead, classic strings and JSX props make it easier to use.
JSX is also used to...
3) Compose components
Don't try to create components with too much structure. Create atomic components that are easy to control and reuse, and instead of adding props for bits of UI, use JSX.
For example, I've already made (and seen) components with props scattered throughout:
<IconButton
icon="success"
iconSize="16px"
iconColor="black"
iconPosition="end"
>Click here</Button>
These props remove control from who will use your component. If customization is required, this person will have to modify the <IconButton/>
, which is rarely a good idea.
If you split responsibilities and support for children, the code will have a clear structure and whoever uses it will have more control over what they want to do:
<Button>
Click here
<SuccessIcon className="icon" />
</Button>
<style>
.icon {
height: 16px;
width: 16px;
color: black;
}
</style>
Over time, some complex components can be structured, always based on these simpler components. New structures can be created based on these atomic components, but these new structures should not stop you from reusing the atomic components.
4) Use headless libraries
We have many UI patterns that HTML does not support natively today and that devs need to implement from scratch. A classic example is the switch component. It is similar to the checkbox, but both its appearance and interaction have its peculiarities that differ from the standard HTML checkbox.
You create a switch from scratch. It's a cool exercise and you do a good job. But it's likely that you didn't handle the component's keyboard and screen reader navigation correctly.
You read the specification of WAI-ARIA and implement accordingly. But now the code has become so complex that the rest of the team struggles following the code, someone else touches <Switch/>
and they feel insecure not knowing if it broke, and the only one who can update this piece of code is you.
The project has dozens of reusable components, each bringing the same pain point. As it grows, it becomes easier to create a new component from scratch than to modify the one that already exists. Your reusable component ends up being not reusable.
Libraries of pre-made components came to facilitate this work. Today there are many styleless libraries, commonly called headless UI, for example React Aria, Radix and Headless UI . They implement the expected behavior and you only implement the style.
You may rather use styled libraries. I haven't had much opportunity to use it, but I know the popular ones: Ant Design, Material UI and Chakra UI.
The beauty of using pre-made components is that you have more confidence the page is accessible, without the need for a lot of code to maintain.
5) Avoid external dependencies
The first reusable components I made were inputs. As the project standard was to use Formik , I created the inputs integrated into Formik.
This seemed like a good decision at the time. But as the project grew, there were cases that did not align with this expectation, and several of the inputs did not need Formik. This added unnecessary friction that was increasingly difficult to deal with, and made it difficult to migrate to alternative libraries to Formik.
If your component is truly reusable, it should have no expectations from its context. The more context-free it is, the more reusable it is.
We can extend it to any logic external to the component. State management (redux), themes (styled components), validations (react-hook-form), layout (flex/grid), and other dependency injections. All integrations must be injected by props or JSX to avoid blocking your component.
Extra: 6) Migration
You might be inspired now, but it's possible that your project already has a reusable UI. Changing these components that are in widespread use is complex, and this discourages you from switching to better code.
But I'm here to say: don't give up on change. Instead, take one step at a time:
Bring the idea to your team. Run proofs of concept, show the value of the changes, and avoid making changes that people don't want to make. Always remember that code is made by humans for humans.
Try to adapt your changes as much as possible with the code that already exists. Some changes add a lot of value to the project, and other changes don't have as much impact. Choose your focus to migrate easily and avoid disrupting workflow.
Think about a migration plan. It is important that everyone on the team is aligned. Add new code, help people use it, deprecate old code, and little by little remove old code from your base.
Putting it all together
Finally, an example of a basic <Button/>
:
import React, {
ComponentPropsWithoutRef,
forwardRef
} from 'react'
import {Link} from 'react-router-dom'
import {Slot} from '@radix-ui/react-slot'
import clsx from 'clsx'
import style from './button.module.css'
type Props = ComponentPropsWithoutRef<'button'> & {
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, Props>(
({className, asChild, ...props}, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={clsx(style.button, className)}
ref={ref}
{...props}
/>
)
}
)
Explaining the code above:
- It is a component that accepts all generic attributes as props
<button/>
. - It accepts a ref value
- It implements a class
.button
that adds project-specific styles - I've used the Radix
Slot
component which helps me applying the button style to another component, it can be enabled using the propasChild
<Button className="my-class">See details</Button>
<Button type="submit" ref={buttonRef}>Send info</Button>
<Button asChild>
<Link to="/login">Enter</Link>
</Button>
In the end, reusable code helps a lot, but it is an abstraction and therefore must be well thought out. Creating reusable code that doesn't adapt to all contexts can create a snowball. It is important to use already recognized standards and give devs control to use them in unexpected ways.
Top comments (0)