This website uses cookies. By using the website you agree with our use of cookies. Know more

Technology

Lessons learned when building product-agnostic reusable UI Components

Sandrina Pereira
A web lover who helps turning ideas into accessible products on her Vans.
View All Posts
Lessons learned when building product-agnostic reusable UI Components
At Farfetch we host and work on several websites like Farfetch.com, our Farfetch Black & White Solutions websites and other back office applications. All of them have one thing in common: the user interface components. One of the responsibilities of the web development team at Farfetch is to keep UI components updated, based on requirements received from the product team.  It’s a work in progress where we aim to balance between efficient code and meeting deadlines. Over the last year, we’ve been redefining our mindset when it comes to building reusable components to better accommodate everyone’s needs. This mindset is helping Farfetch to keep reusable UI components product-agnostic.

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 } />

It didn’t take long for that slideshow to receive the first change requests, just for mobile, by another team. That team added a new prop
isMobile
which 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.

In retrospective, what went wrong?

Because most of the times we start building a reusable component based on requirements established by the product team for a specific team or scope, we unconsciously end up creating poor component APIs, that is, poor props definitions. In addition to this, we tend to start implementing the design specs without asking the right questions to our fellow designers or bothering to understand the purpose behind the layouts received. That’s the perfect recipe to develop an inconsistent, non-reusable component.

A better approach

First things first: Think Global

When 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 
isMobile
prop 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

Once we know how to correctly name a prop, the common next step is to use props everywhere - the Apropcalipse - a component with too many props. When in doubt, prefer to break a big component into smaller ones that are responsible for only one piece of your feature. We could break the Slideshow as it follows:
  • 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.
This list can grow as the slideshow features grow too and it’s sustainable to maintain each small component because all of them follow the single responsibility principle. Let’s refactor our slideshow to follow this mindset:

// 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.
  • Simplicity: With more explicit APIs, the learning curve to use a reusable component drops. Say goodbye to prefixes in the props such like arrowClassName, renderCustomArrow and onArrowClick. Tip: Prefixes are a red flag to detect when a component has too much responsibility.
  • Less Responsibility: By taking away responsibility from a reusable component you are trusting your teammates, providing UI freedom, and reducing the number of concerns on your side.
  • Performance: Since only the necessary parts are imported, the tree shaking will do the rest and the bundle size will be smaller.
  • Maintenance: With less code, we have fewer bugs. Also, the friction to the new contributors drops as well.
  • Unit Testing: Usually unit tests reflect a component’s complexity. The simpler the component, the simpler the tests.

small components = flexible API = easier to (re)use

The difference is in the details

Testing as a safety measure

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 as a best friend

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

Before jumping into the code, start by defining the component API with a pen and paper or in a whiteboard (this is teamwork!). Always remember to ask yourself if that prop is related to the UI Logic and not to the product requirements. A great agnostic API is what makes a truly reusable component. Work closely with the designers to better understand the component’s purpose and foster a common language and better communication.

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
Thanks for reading!
Related Articles