Lessons learned when building product-agnostic reusable UI Components

What are the struggles of creating reusable UI components?
Spotting new opportunities for reusable components (ex: a slideshow or an accordion) is the easiest part: you build it to suit your specific case and everything seems fine. After additional changes from different developers to suit other cases, it becomes non-trivial to update the now messy code. Meanwhile, a newcomer web developer creates a slightly different version to avoid breaking the original one. That’s when you realise the component is not that reusable after all and the worst part is that nobody knows exactly what went wrong.A common but poor approach
One of the reusable components we typically use is a slideshow. However, this story can be applied to almost any component. One of our teams was the first to implement slideshows with a minimalist outcome:
<Slideshow slides={ images } />
isMobilewhich when true, reflects the requested changes. Still simple, right?
<Slideshow slides={ slides } isMobile={ isMobile } />
After different feature updates made by different developers with different product requirements (add or remove features, A/B tests, styling, you name it…) we can easily end up with a component as uncomfortable as the following:
<Slideshow
slides={}
isMobile={ }
isInfinite={}
slidesToShow={}
hasKeyNavigation={}
speed={}
onAfterChange={}
onClick={}
hasArrows={}
arrowsClassName={}
renderCustomArrow={}
paginationVariant={}
paginationClassName={}
bulletsOnClick={}
hasThumbnails={}
thumbnailTitle={}
thumbnailAlt={}
onThumbnailClick={}
onThumbnailHover={}
pauseOnThumbnailHover={}
// etc...
/>
This is what Jenn Creighton calls "Apropcalypse”. A newcomer will have all the reasons to copy and paste this component saying "This component is too complex, I just need 1/10 of those lines”. When the struggle is real for those who want to reuse a component, it means it’s much worse for the maintainers to refactor it.
An Apropcalypse happens when we have too much implementation complexity and still lack usage flexibility.
A better approach
First things first: Think GlobalWhen building a reusable component, it’s not just a matter of doing it with your teammate (usually allocated to a specific scope). It’s a collaborative effort of a team with a global vision of the product where designers and developers must cooperate. You must work together to analyse the component’s purpose: where and how it will be used. This is a crucial first step if you want to build a sustainable reusable component.
Separation of Concerns principle
While building interactive UIs, one of the principles we should always have in mind is to keep the separation of concerns between business logic and UI logic, aiming for an agnostic and flexible component API, decoupled from the product requirements. If it has product requirements inside it, that’s a red flag reflected right in the component API. It means the component may not be that "reusable” after all.
Naming props the right way
Let’s go back to the first slideshow change request for mobile that caused the
isMobileprop to be added. These were the specs: "The slideshow in mobile should not be infinite and should have arrows only when it has more than one slide."
<Slideshow slides={ slides } isMobile={ isMobile } />
This is a common on-demand tech solution: a reusable component API mostly based on the product requirements. The usage is still simple and everyone in the team knows what it is about. But, deep inside, we all know that the slideshow file is getting messy and full of if statements that have nothing to do with the slideshow itself (ex: if on mobile, it’s infinite). When a reusable component has conditions based on product requirements, probably that component won’t last long.
A reusable component API must be intuitive and based on the layout. The product requirements should stay out of it.
A common exercise that helps better naming props is to put yourself in a newcomer’s shoes. Let’s try it now: "Would a newcomer guess what each prop does without knowing the product context nor needing to read the component source code?".
<Slideshow slides={ slides } isMobile={ isMobile } />
/* Newcomer possible line of thought:
"The slideshow accepts slides. On mobile something happens, but I have no idea what it is.
*/
Usually, we were told that a reusable component should have a simple API - the fewer the props the better - a simple API doesn’t mean a short one. It means it should be intuitive and scalable. For a newcomer, the component’s API is not intuitive and they would need to read the slideshow source code to understand it. When there’s a new update to a slideshow, we should not be surprised if a newcomer simply copies and pastes the code and implements a slightly different new feature just for their use case. We’ve all been there: the fear of touching and breaking someone else’s code.
When you need to add a new prop, you should be asking: "What does it do? Does it have arrows?
hasArrows. Is it infinite?
isInfinite”. So, let’s rename these props to:
<Slideshow
slides={ slides }
isInfinite={ !isMobile }
hasArrows={ hasMoreThanOneSlide || !isMobile }
/>
/* Newcomer possible thoughts:
"The slideshow accepts slides. It’s infinite on mobile. It has arrows when there's more than one slide or when it’s not mobile.
*/
A prop name should answer what it does (it’s infinite) instead of when it does (it’s on mobile). With this approach, the name and its value now reflect the product requirement right away - it’s what defines an intuitive component API. Do not let product requirements mess with the implementation of a dumb component.
Single Responsibility Principle
BaseSlideshow
: contains all the components.
Slider: responsible for the slides movement/animation.
Arrow: displays the button to the user click next or prev.
Bullets: displays the dots corresponding to each slide.
Thumbnails: displays a list of small images representing each slide.
Pagination: displays numeric pagination with the current active slideshow.
// import only the needed modules of a slideshow...
import { BaseSlideshow, Slider, Arrow, Bullets } from 'slideshow';
// ...arrange them as you need...
<BaseSlideshow>
<Slider
slidesToShow={ 2 }
onAfterChange={ handleChange }
>
{ images.map(/* ... */) }
</Slider>
// ...with flexible customization
<Arrow flow="prev" onClick={ sendTracker } />
<Arrow flow="next" />
<Bullets className="dots" />
</BaseSlideshow>
This is the Compound Components pattern and this is one of the many patterns you can use when building UI components. These are the main benefits:
Flexibility: Now it’s up to your teammates to define how they want the slideshow to look: What are the parts needed and how to arrange them in the DOM.
The difference is in the details
A reusable component is not just about the component itself. When used and updated frequently by multiple developers, the risk of breaking is high. To avoid bugs after a release, make sure to write unit tests that cover the UI logic and guarantees the DOM output is correct.
Documentation is what makes a reusable component great. Farfetch has a lot of developers and designers, so, documenting components has a great impact for collaboration and efficiency. It’s also a nice way to think through its details. Solid and detailed documentation with code examples is a must-have. It will save a lot of time and pain when your colleagues start to use the component. Storybook and React Styleguidist are awesome tools to help you build great interactive documentation for your components.
A recommendation for the next time
Don’t expect to get it right at the first try. It’s always a work in progress. The trick is to collaborate in a team with a global vision without trying to predict all the possible next cases. Remember, done is better than perfect and the future is unpredictable, so start small, keep an eye on its evolution and we’ll all be fine.
Resources
"Simply React” video talk from Kent C. Dodds.