Adventures in Packaging Existing React Components as an NPM Library

At work, I have been working on making our UI components more reusable. Well, nothing new there since I have spoken and written about making CSS more reusable in the past. But as Viki grows, so does its services. Making UI components reusable across a single app is no longer a challenge. Instead, we need to focus on making our components shareable across multiple apps.

I want to share my thought processes and experience in turning our existing React components on Viki and Soompi into an NPM package. This is more of a guide rather than a tutorial since tools and its behaviours change really fast in our industry. I will be focusing on React components here but this experience can be applied to any UI building libraries out there.


Pre-production is Crucial

Prior to working on the NPM package, I thought about several important features that the library must have:

  • The library must support server side rendering. Server side rendering is important to apps like Soompi for SEO since it is mostly read-only.
  • CSS files should be exported from the library so that styles can be reuse.

Planning out the features and behaviour of the library is important since it helps me pick out the right tools prior to development. I try to research tools as much as I can at this stage because I know how frustrating in can be to settle with a particular tool only to realize that it is not the right one and having to change it later on. 

Picking Out the Right Bundler

For cross environment friendly library, I decided to bundle the module into UMD format. Initially, I decided to settle with Webpack since it is something that I am already familiar with. Unfortunately, Webpack bundled UMD modules doesn’t run properly on server side. You will either get window or document is not defined. At this time of writing, this bug hasn’t been fix yet. You can make it work but you do need to add in extra configurations which I am not interested in exploring. So that’s when I decided to use Rollup which is touted as being more suitable for bundling libraries.

Since all of our components are already written in ES2015 which is the very specification that inspired Rollup in the first place, working with it doesn’t generate much fuss. But I didn’t just install Rollup and call it a day. There are several things to consider; things that I normally do not think about as a non-library author:

Allow library consumers to take advantage of ES2015 features

Serve UMD/CommonJS and ES2015 together and use pkg.module to refer to the right bundle. pkg.module would point to your ES2015 build while pkg.main would point to UMD/CommonJS code. That way, library consumers using ES2015-aware tools like Rollup can take advantage of ES2015 module features (e.g., import/export for tree-shaking) while others on Browserify or older versions of Webpack can continue using legacy module formats.

Listing certain modules as peer dependencies

React, ReactDOM, and other dependencies that are expected to be used by library consumers should not be bundled as part of the library as it will create conflict if the dependency version used by the consuming project and your library’s are different. They should be listed under peerDependencies instead. By using peerDependencies, your library will use the dependency that is part of the consuming project instead of another version that is private to your module.

You might not need UMD

UMD builds are larger in file size since it includes the whole shebang of code that is suppose to be able to run everywhere. In certain cases, bundling it into CommonJS format should be enough and if you need a browser friendly version that can be included through a script tag, bundling a separate IIFE version should do the job. Theoretically, CommonJS format should suffice for a cross-env friendly React component library as long as I am not exposing any browser specific object in the render method. I could be wrong though and I haven’t investigate this much but it is worth keeping in mind.

// Your package.json should look like this
{
  "name": "Marauder",
  "version": "0.0.1",
  "description": "I solemnly swear that I am up to no good.",
  "main": "dist/lib.umd.js",
  "module": "dist/lib.es.js",
  "peerDependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  }
}

Providing CSS for Shareable Styles

Since the components across our sites share the same look and feel, it is important for us to ship the CSS alongside our React components. But there are several ways to do this and it all depends on your workflow.

Scouring articles about distributing React components through NPM, I noticed there are many articles that recommend using CSS-in-JS in order to ship stylings. The arguments are compelling since it makes your code truly modular. Styles are defined within the components itself instead of maintaining individual stylesheets.

I’m a traditionalist when it comes to CSS

Despite all of the praise about CSS-in-JS, I still consider myself a traditionalist who prefers CSS-in-CSS. It is all about familiarity and I am more familiar with best practices of using CSS traditionally. Further, I am dealing with existing components that are already being styled with individual CSS files. Trying to convert that to CSS-in-JS would be a nightmare since there are many code to be refactored, new tools to be learned and a high learning curve to traverse. Although it is true that CSS-in-JS is elegant and makes your code more modular, it doesn’t mean that it is portable since it strictly runs in JS environments. Code portability is important if you’re working in an organization that has multiple projects with different environments.

Shipping CSS traditionally

I decided to bake our CSS as part of the build so that styles can be be imported this way:

// Import compiled CSS directly
// Tilde(~) sign is an alias to node_modules in certain environments
@import '~marauder/dist/css/marauder.css'

But the downside to this is that you will get the entire styles including the components that you are not using. In this case, possible solutions are to either compile each component’s CSS in its own individual file or by importing individual SCSS files directly.

// Import compiled CSS directly
// Tilde(~) sign is an alias to node_modules in certain environments
@import '~marauder/dist/css/carousel.css'
@import '~marauder/dist/css/thumbnails.css'

// Import raw SCSS directly
@import '~marauder/src/scss/carousel.scss'
@import '~marauder/src/scss/thumbnails.scss'

References