Forming Best Practices

As we built out our component library we decided on a number of best practices to introduce into our engineering and review process. Some of these best practices come from experience, and others out of necessity. Taking on this project gives us a chance to do things differently, freeing us from any constraints we had created as we learned more about React over time, providing opportunity to make positive changes without having to re-do everything at once.

Here’s a few of our best practices we’ve introduced:

Components shouldn’t enforce exterior spacing.

When you create a component, that component should be able to be used for as many applications as needed. If the component requires additional exterior padding or margin that shouldn’t be added to the component itself, instead it should be added through an inline style or class when importing it. This prevents the need to create duplicate variants within the component library which apply to the component in one specific scenario. As a rule of thumb we always pass through a style and className prop to the parent element of all components in our library. The exception to this rule is when creating a template which includes either an Atom or a Molecule, these can have their own internal spacing classes which affect the exterior spacing of another.

All components should have prescriptive PropTypes.

We want to make sure that we can self-document our code using PropTypes, even going as far as creating and using custom ones when needed. We also require comments on all of our PropTypes so they can be documented using storybook/addon-info and react-docgen.

Example of commenting PropTypes for react-docgen.

All components should have 100% test coverage.

As we’re building component stories and variants with limited scope, we decided to enforce the rule of full test coverage. Combined with integration testing we can dramatically limit the chance of unexpected bugs making their way into our production code. We also enforced a number of linting rules, including those which cover accessibility concerns.

CSS should be namespaced.

This prevents class name clashing when used in our applications. To achieve this we wrap all of our components in a class name which describes the component such as .uic--ui-loader , and then have additional class names cascade. At first we wanted to auto prefix our class names using css-modules, but ultimately ended up doing it manually as we weren’t able to run some of our external dependencies through it which power our grid and other utility classes.

Example of namespacing CSS.

Stories should be staged appropriately.

Storybook provides a way for users to click through all the different variants of a component. These are called stories. In the case of a Dropdown component it should have a story for its closed state, open state and any other type of state such as error. This allows us to easily see the visual variants of the component, and ultimately run visual tests against them.

The following example shows one of our illustration components which has multiple different color states.

Example of staging a story in Storybook.

Structuring a Component

All of our components within the library are structured following Atomic Design principles. Any kind of basic component is an Atom, and things with more complexity are either a Molecule, Organism or a Template. We structure our components folder as follows.

•

└── components

├── atoms

│ └── Input

│ ├── Input.js

│ ├── Input.md

│ ├── Input.scss

│ ├── Input.spec.js

│ └── Input.story.js

└── molecules

└── organisms

Each component has a series of files associated with it: in most cases a JavaScript file with the exported component, a README file, a spec file for testing, an scss file for styling, and a story file which renders the component within Storybook. While we require our PropTypes to be commented, we also include things such as implementation specific instructions within the README. Our goal is to make the library as appealing as possible for our team so they are encouraged to both use it and contribute to it.

As the component structure is predictable we even came up with a way to automatically generate a new component using a node module called hygen. This allows us to simply type yarn generate in the terminal and answer a few simple questions to get a boilerplate up and running for a new contribution.

Running yarn generate.

Visual Testing Deployments

One of the biggest nightmares of working with CSS is having to deal with visual regressions. Too often someone changes a single line of CSS, and it creates a visual change which goes unnoticed for days, sometimes even weeks. As we stage the components and their variants in their stories we can use these consistent states to create a visual diff on each release, allowing us to keep track of visual changes. Combined with namespaced CSS and 100% test coverage, this allows us to release with confidence each and every time.

Percy detecting a change.

For our snapshot testing we use an integration called Percy to keep record of how our components are changing. Setting it up to work with Storybook occurs in the Storybook initialization file.

Example of configuring Storybook with Percy.

For stories with animated elements, we can tell Percy to skip the visual diff testing as it would flag it on every review. We mostly have to do this for components which have animated SVG’s such as our loading spinner.

Example of hiding a story from Percy.

As you can see it’s catching the animation.

We configured the snapshot script to only run when a pull request is made from our develop branch to the master branch. We did this as that’s the final push in the chain before a new release is cut, which ultimately makes its way to our production build after being published to npm. We also felt it was overkill to test on every push to the develop branch as we tend to move quickly and sometimes merge work in progress pieces.

If a visual diff is found, the pull request gets blocked (similar to how other CI tools work) and it flags our designer to sign off on the changes before the merge request gets green-lit.

Percy blocking a pull request.

What We’ve Learned

We’ve learned a lot while creating this library, below are a few key points.

Managing dependencies correctly can be difficult.

Managing dependencies appropriately was a lesson we learned quite quickly, when we discovered that duplicated dependencies were making their way into our application through our component library. After auditing our component library module with the webpack-bundle-analyzer tool we discovered the entire lodash library being included many times. This resulted in a lot of bloat making its way into our main application which began causing performance issues.

Everyone gets a lodash.

Correctly marking modules as peer dependencies and marking the modules as external in our Webpack configuration helped resolve this.

Much better.

Making sweeping changes can be difficult!

Even though we’ve built out a component library, making changes to existing components within our main application was difficult. The first component we built was a button, and replacing all of our old buttons with the new one required a lot of engineering effort.

We even built our own makeshift visual integration test just so we could get some automated visibility over a number of scenarios in our application to minimize the impact of replacing this one button component which was referenced over one hundred times. Our visual diff tool was similar to how Percy operated, except took a snapshot with Puppeteer every time our Selenium tests paused, it didn’t work great and therefore didn’t make its way into our deployment flow, but we keep it on the back-burner just in case we need to recommission it.

The impact of changing a single button located within the step 9 box.

Even though we jumped through all the appropriate hoops we still needed to hotfix a lot of issues. Fortunately we expected a degree of that and tried to prepare ourselves as much as possible.

We’re certainly not perfect!

Closing Thoughts

Overall creating a component library was a positive experience, and we’re excited about the benefits it will bring to how we build components at United Income going forward. We’ve decided to open source our component library as we believe it could be helpful to others. You can view our Storybook instance here, and the code on Github here. 🎉

If you’d like to learn more about United Income you can follow us on Twitter or LinkedIn, or you can follow me, the author of this post, here.