Reusable Components... That aren't so reusable!
For many years I've wanted to share my thoughts around many things related to dev and code. Until now, I've always made excuses for myself and never gone for it. Well, here I am!
You've probably figured by the title, but I just wanted to collect and sound out my thoughts on building reusable components that, unfortunately, don't tend to be reused effectively.
Here's a scenario that I've fabricated to highlight my point on component reusability. For simplicity, let's assume that no component libraries are being used.
You've been asked to deliver a new feature on a React project. The requirements are:
- On a screen that already exists, a new modal is required
- The modal should appear when the user has visited the site page 3 times
- No need for backend integration, this can be handled locally
- The modal should have a close cross in the top right
- The modal should contain some text and a button in the footer that will also close the modal
Great, a nice easy feature that you could get over the line fairly quickly, with few implications elsewhere.
An uncomplicated implementation of this feature would be to utilise a
useEffect hook to check a local storage value on page load to see if it's the users third time visiting, if it is, you can set a
useState hook to a truthy value.
Within the render, you can then conditionally render a simple
div with the necessary content and button.
onClick event of the button that sets the
useState value to false and then that's functionality complete.
Assuming the tests are adequately written and accessibility has been considered, that's about everything finished for the request.
So you're presumably asking yourself - "Yep, what's wrong with this?"
At the moment, there's nothing wrong with this implementation, however, in the future, this could easily become a pain point.
So some time has passed and since our last implementation the code for the modal has been abstracted to an external component and it's now been used in four different places. Great, we have a reusable component that's been reused! Kind of.
There's been a few other features that have now been implemented too. A couple of these being a popup image lightbox and a contact form in a modal that can be opened from any page. You can probably see where I'm going with this...
Implemented with just a simple
div that's conditionally rendered based on a
useState hook value. This has all the functionality of the lightbox, with just a Close button to the top right of the modal.
Contact Form Modal
Implemented with just a simple
div, again, that's conditionally rendered based on a
useState hook value. This modal renders a form and a simple Close button to the top left.
What went wrong?
From the distance at a unit level, everything can appear okay, however, this is far from reality.
What's happened here, albeit exaggerated, is we now have three different implementations, that have been built by three different developers.
This, of course, isn't just the opening and closing of a modal. We also have to consider:
- Unit tests
- User experience
- Development time
- QA time
Surely just a modal can't have such an impact? Let's take a quick walk through each point.
We have three different sets of tests that are testing things slightly differently, which naturally decreases confidence as there isn't a single spec for a single piece of functionality.
Accessibility is important and if not implemented correctly can impact users. Having three bespoke implementations of a single piece of functionality means there's a higher risk of something being missed.
For example, a bug is raised where an
aria-label is required, it gets fixed in two places, but one gets missed. A, potentially important, part of the user journey is now impacted.
Because each implementation is slightly different, styles vary slightly. One of the implementations features a
fixed positioning, whilst the others were accidentally broken on scroll because they used
Because there are styling, accessibility and implementation variances, this means that the user experience is going to be impacted.
One moment, to exit the modal you use the cross in the top right, the next it's now in the top left. For a user, this can be confusing and frustrating.
Development & QA Time
I'm going to combine both of these into one. The building, maintaining, testing and optimising three different implementations is inevitably more pricey than doing it correctly just once.
With all of the above in mind, exposing the codebase to varied implementations can only increase the risk of things going wrong and being more expensive to fix in the future.
How it could have gone?
The above problems could have been solved easily, just with little upfront planning and preparation. I would have suggested that when the second requirement came for a modal implementation, that it would be an ideal time to split out and abstract consistencies.
Notice how I said the second requirement. I have observed so many times, DRY (Don't repeat yourself) being used prematurely and it can be very painful for a team to develop everything in such an abstract way that never gets used.
Looking across the four implementations we have a clear consistency (of course), the modal functionality. The opening, rendering, closing of a modal box that has some sort of content/feature within it.
This is a perfect example of where the code could have been extracted out of the initial implementation into a generic component that could be reused multiple times. We could have not only taken the implementation of the opening, rendering and closing; but more.
We could have abstracted out the styles, tests, that little cross button in the top right corner, the accessibility features. You get the idea, all of the duplicated code and implementation could be in one single place. Doing this would then have alleviated all the pain points that I outlined above.
Now you can see why I mentioned that the original modal implementation was only semi-reusable. It was heavily tied to a certain bit of functionality that had a very specific case. But the base modal component, couldn't be reused so ended up being duplicated.
As I mentioned beforehand, the example that I provided above is pretty improbable. However, it does highlight what can happen - hopefully not this exact case.
I have seen so many cases, where components have been copied and pasted and then manipulated to integrate somewhere to offer something else that’s similar, but not the same, leading to headaches further down the line and confusing code.
I’d unquestionably never advocate doing the opposite either, which is prematurely doing these kinds of abstractions. This can and will have its pain points which are equally as challenging.
A lot of this can sound really silly and quite obvious, but it is crazy easy for people to fall into the trap of just rebuilding something rather than abstracting and reutilising.
There's not only a development gain to making your components reusable, but there's also advantages for your users and the business too; as long as it's not done too early, or too late.
Abstract duplicate implementations into separate, standalone, generic, components.
But not too early
Don’t prematurely abstract though; only do it when there’s a necessity for further implementations.
Plan your implementation
Splitting out your implementations at different levels has benefits that aren’t just development related.
Adhering to the single responsibility principle helps isolate units of code and means that the possibility of accidental code duplication is more limited.