How We Built a Shared UI Component Library
Our process and lessons learned in designing and developing an internal component library.
I’m a member of the Braintree Sites team. We’re responsible for Braintree’s externally facing sites, including braintreepayments.com, the developer docs, support articles, and the new GraphQL docs. While all of these sites live on the braintreepayments.com domain, they are actually three different applications.
We try to maintain consistency across our various properties. In fact, our designers are working on an initiative called Design Align by syncing our styles across all the Braintree websites. But let’s face it, aligning design becomes pretty tedious when you have several of the same (or same-ish, tbh) components implemented on various pages across three different applications. If we want to make a simple change — say, increasing a button’s border radius by 1px — we need to find every instance of that button and make sure it’s been updated correctly.
Enter shared components — what I’m going to talk about today.
In this blog post I will walk through the initial strategy behind our internal shared component library, Sites-UI. I’ll also discuss what we learned and changed along the way.
What is a component and how do you share it?
For our purposes, a component is some self-contained UI element originating from one source file that can be popped into different locations in your code and still work as intended. A button is a great example. Let’s say our designer creates a button style that should look the same across all of our properties, but should do something unique when the user clicks it from different locations. We would simply have that component live in one location, then import it into the target application with a dynamic
onClick property that can be passed in wherever the component is rendered. Then when we want to change the border radius by 1px, we only need to make the change in our library and update our consumer applications.
There are many ways to share a component. If you have a single application, you can keep a shared components directory within that application. But the real value, in our case, is being able to share components across multiple applications. With this purpose in mind, we created an internal shared component library that can be imported into each application. A component can then keep the same structure and styling everywhere, but handle any application specific needs through dynamically passed properties (like the button click).
Before I joined the Sites team four months ago, the team had already decided on the necessity of a shared components library. From the moment I came in everyone was fully on-board to realize the shared component dream.
Company and team buy-in? Check. JS framework decided? Check. Let’s talk about building the thing!
To create-react-app or not to create-react-app?
First, we needed the React application to house our shared components. For those unfamiliar, running
create-react-app on the command line generates a configured React app right out of the box, with all the dependencies you need to get started.
Initial verdict: Write it from scratch
Verdict after two months: Use
I did learn a lot from setting up the application from scratch, but ultimately I ended up rebuilding a lot of the functionality of create-react-app. I had a lot of problems with dependencies, did a lot of Googling, and finally installed react-scripts, the dependency behind create-react-app, which ended up solving my problems with minimal stress.
How to bundle: Parcel vs Webpack vs Rollup
A JS bundler takes all your files (code and dependencies) and puts them together into one JS file for the browser to read.
Conventional wisdom used to say “use Webpack for apps and Rollup for libraries”. However, as both Webpack and Rollup have matured (and throw Parcel into the mix as well), the decision as to which to use becomes more difficult.
All three bundlers include tree shaking: the removal of unused code (though this feature is only “experimentally supported” in Parcel). All three also support code splitting: splitting of code into separate bundles allowing consumers to dynamically load them as needed. The main differences among the bundlers are speed and ease of configuration.
Initial verdict: Start with Parcel, upgrade when needed
Due to Parcel’s zero configuration, we could get up and running fast and easily. Upgrading to Webpack or Rollup at a later date would be possible.
Verdict after two months: Don’t use Parcel unless you really know what you’re doing
Parcel is, indeed, zero configuration, which I didn’t believe until I saw it with my own eyes. It’s pretty amazing, but it comes at a cost, which I learned pretty quickly while building Sites-UI. The hard truth is that the JS world is not built for Parcel; the JS world is built for Webpack. Any Parcel issue you run into will be very difficult to solve based on internet research. Every problem I ran into had a solution for Webpack (due to its maturity and strong community of developers) but almost never for Parcel. While Parcel would, on the surface, seem good for beginners based on the zero config thing, I would argue that this difficulty in troubleshooting makes it less accessible to newbies.
Once we got everything up and running with Parcel, it seemed silly to switch to Webpack. We did, however, add Rollup for distribution, given its reputation for playing nice with libraries, while keeping Parcel for development.
Type checking: PropTypes vs TypeScript
React applications are comprised of components which allow for passing data via props. As an application grows and more developers dive in, guardrails such as type checking help ensure we are passing the correct data around to reduce errors.
At one time the React Core module included PropTypes, but it has since been removed. It’s an easy-to-learn, lightweight option for documenting types, but it does not enforce types, so its usefulness is limited.
Initial verdict: None (surprise!)
PropTypes doesn’t do much, as it doesn’t enforce types, so the decision came down to TypeScript or nothing, and I ended up choosing nothing. I did not have much experience with types (coming from a Ruby background) and I wasn’t convinced of our need for a type checker given our applications’ intended usage as a lightweight, internal only utility.
Verdict after two months: TypeScript
Okay, I came around! Certain people around here are hardcore TypeScript evangelists, and I wanted to see what all the fuss was about. The main reason why I changed my TypeScript tune comes down to runtime errors. I could live very happily never seeing another one of these:
TypeError: Cannot read property ‘foo’ of undefined. While we don’t see many of these errors coming from our Sites-UI yet, I would expect to see more as our application grows and expands into production. I hope TypeScript will help us solve this problem (fingers crossed).
The truth is that types are a reality of programming, whether you’re using a statically or dynamically typed language. We may as well be explicit about types, rather than pretend they don’t exist. In the words of the wise Sansa Stark, “I’m a slow learner, it’s true. But I learn.”
OMG! What about CSS?
Dare I say that CSS is the most divisive front-end issue of our time? Specifically, whether or not to put CSS in JS. Just Google it, you’ll see.
The decision we were facing was whether to use a CSS-in-JS library, like emotion or styled-components, or CSS Modules. CSS-in-JS would be something new for our team and Braintree as a whole, as everyone is currently using CSS Modules, SASS, or just plain CSS.
Initial verdict: CSS Modules
I like standalone CSS and think it works very well as-is. Plus, the addition of CSS Modules solved the problem of global scope. I also see the value of reducing the learning curve for devs and designers who regularly work with CSS. Given our needs and codebases, CSS-in-JS introduced unnecessary complexity for limited benefit.
Verdict after two months: Still hanging on to CSS Modules for dear life
I find my personal position (pro CSS Modules) getting weaker. Another dev here just did a spike to add emotion to Sites-UI and I’m going to evaluate it fairly. I think there’s something to be said about the tried-and-true approach, but we also need to keep pressing forward. Maybe CSS-in-JS is a great approach, maybe not. Let’s keep an open mind.
Why Storybook is the coolest
Storybook provides a UI to view components in the browser, without importing and rendering them in an application.
I’ve found myself loving all the Storybook “add-ons,” specifically Knobs to dynamically add props, Viewport to preview multiple screens sizes, A11y to check accessibility violations, and Info to document components.
There were really no decisions to be made here; we’re just using Storybook because it’s freaking awesome. It’s also the industry standard and used by other teams here at Braintree.
Initial verdict: Storybook
Verdict after two months: Storybook
How will we test the library?
There are two questions here: 1. What kinds of tests do we want to write and 2. which tools will we use to write those tests? We considered unit tests and integration tests, plus additional snapshot testing. Snapshots tests the output of a function, specifically a component tree in React, by taking an initial image of a component in a specified state and then comparing future outputs to that image. Obviously if you make an intentional update to a component, you will need to update the snapshot too.
Initial verdict: Snapshot tests and unit tests with Jest
There are a lot of testing suites out there for React. I suggested starting with Jest for both snapshot testing and unit testing. Jest is the React industry standard, built by Facebook specifically for React, and also recommended internally by Braintree.
As components are meant to be modular, it makes sense to test their functionality (and structure) in isolation. Integration testing could be accomplished by creating a sample application utilizing the UI components and running the tests against it. However, since all of our components are meant to be imported into any application, it makes sense that they should work as individual units, making unit testing more valuable. Integration testing should be saved for the app that utilizes the component (which is exactly what we ended up doing).
Verdict after two months: Same as above
In addition to using Jest, we added React Testing Library for additional features.
This is definitely an area I hadn’t thought about before starting on this project. How will we make Sites-UI available to developers? The most common way is to publish an npm package, but we could also use a repository on our internal GitHub Enterprise.
Initial verdict: GitHub repo… for now
We do not need the component library to be available for public use, so there’s not a strong reason to take the extra step of publishing to npm. A git repo is the simplest solution for now, and we can explore npm if we decide it’s necessary.
Verdict after two months: Still using that GitHub repo…
And I don’t see that changing in the near future.
This was also an easy decision. Current Braintree libraries use semantic versioning, a common industry practice. Semantic versioning utilizes MAJOR.MINOR.PATCH system for incrementing the version number.
- MAJOR version is for breaking changes
- MINOR version is for feature additions
- PATCH version is for bug fixes
Versioning does get fuzzy when discussing UI changes, as MAJOR.MINOR.PATCH becomes largely subjective. For example, if we change a button color from white to black in our library, it would seem like a MINOR change. However, if you import that newly black button onto a black background, rendering the button invisible, this could be considered a breaking change for your application.
In our case, such changes require coordination across properties, so we need to make sure to have an agreement on the signals of our versioning. It will be important to have clear documentation around what we consider to be within each category, and since we can’t always cover every scenario in our documentation, we should also err on the side of more aggressive version bumping as a cautionary measure.
Initial verdict: Semantic versioning
Verdict after two months: Same as above
At this point we’ve got a basic versioning script in place, using npm-version, that runs the tests, builds the lib directory, tags the version, and pushes the tags to GitHub. We’re still importing from GitHub, rather than publishing the package, but semver works with GitHub too! Our version is still 0.0.0, but I think we should be bumping soon!
To sum it all up
As you can see, this was quite a journey for me. I went from knowing only how to use a JS library to actually building one from scratch. The research gave me a lot of initial ideas about which tools to choose, but, in the end, experience is what made the real decisions. Only by implementing the project did I see which tools would work for our purposes. As you read above, I changed my mind a lot — and I think that’s a good thing! I have a sticker on my laptop that says “Strong opinions loosely held” and I try my best to achieve this ideal. Passion and conviction show strength, but reasoning and adaptability show growth.
Have you written a shared components library? Were there any other considerations that you had to think through? I’d love to hear from you about any interesting obstacles that you ran into and how you overcame them.