Guide to building a React components library with Rollup and styled-jsx.

I have been working on a component library for React. I styled my component with styled-jsx — a simple CSS in JS library that allows you to write scoped and regular CSS in a way that resembles ShadowDOM. As I was looking for ways to package my components, I found that Rollup was a great tool to get the job done neatly and quickly. In this blog post I will walk you through how to package a simple HelloWorld component. 
Feel free to check out the final result on github.

Starting from scratch

We have a HelloWorld.js component and our goal is to create a library which we can later publish to the npm registry, so that users can consume it in their apps. Here is the component under src/HelloWorld.js:

import React from "react";
class HelloWorld extends React.Component {
constructor(props){
super(props);
this.state = { message: "Hello World!" };
}
  render() {
return (
<div>
<h1>{this.state.message}</h1>
<style jsx>{`
h1 {
color: red;
}
`}</style>
</div>
);
}
}
export default HelloWorld;

Initial project setup

Once we have created package.json with npm init, we need to declare few dependencies. At this moment, we agree that we’ll use Rollup, React, and styled-jsx. Let’s install it:

npm i -D react react-dom rollup styled-jsx

We also need a way to transpile JSX syntax in our files. We would like to use the latest EcmaScript features. This is what @babel/preset-react and @babel/preset-env do in this order. They both depend on the @babel/core module being installed, so we need that too:

npm i -D @babel/preset-react @babel/preset-env @babel/core

Ok, great. We need just a few plugins to make everything work together:

npm i -D rollup-plugin-babel rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-replace

{
"devDependencies": {
"@babel/core": "^7.3.3",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"react": "^16.3.1",
"react-dom": "^16.3.1",
"rollup": "^1.2.2",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-replace": "^2.0.0",
"styled-jsx": "^2.2.6"
}
}

We have our key dependencies in place. We can now create our configuration.

Configuring Babel

.babelrc is a configuration file that will be later consumed by babel-core and used by Rollup to transform our JavaScript files. We’ll set our presets and require the styled-jsx babel plugin:

{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react"
],
"plugins": [
"styled-jsx/babel"
]
}

The “modules”: false means we don’t want modules to be transpiled to CommonJS format by Babel.

Configuring Rollup

We need to define which file to bundle, where to output it, and which format we are going to use:

input: "./src/HelloWorld.js",
output: {
file: './lib/dev.js',
format: "cjs"
}

We’ll create a CommonJS library, as it is the most popular format and can be handled by NodeJS. The next thing to do is:

  1. Set an environmental variable based on process.env.NODE_ENV.
  2. Teach Rollup how to use Babel.
  3. Resolve imports from node_modules.
  4. Transform CommonJS to ES6 modules that Rollup can then handle.
plugins: [
replace({
"process.env.NODE_ENV": JSON.stringify(NODE_ENV)
}),
babel({
exclude: "node_modules/**"
}),
resolve(),
commonjs()
],

As you can see, we excluded node_modules, because we expect them to already be transformed.

External dependencies

If tried to run the configuration with npx rollup -c, it would create a bundle under lib/dev.js. That’s nice, however, this bundle would have lot more code than our simple HelloWorld.js component. We’re assuming that both React and styled-jsx will already be installed in the app that uses our library, though, so we don’t want to bundle code that will be duplicated. First, let’s state that in the package.json:

"peerDependencies": {
"react": ">= 16.x.x",
"styled-jsx": ">= 3.x.x"
}

If a user tries to install our package without having the dependencies above in place (and the correct version of them), npm will throw a warning. Now, we need to tell Rollup to skip bundling these dependencies:

external: id => /^react|styled-jsx/.test(id)

Next, if we run npx rollup -c again lib/dev.js should get a lot smaller! That’s because Rollup will treat React and styled-jsx as external dependencies and won’t bundle them with our library.

Optimization

Some libraries behave differently when running in different environments. In the production environment React will skip some validations, Prop-Types will not throw errors, styled-jsx will not include source-maps, and so on. So in addition to the development bundle, we need to deliver an optimized version of it when our library is used in a production environment. To do so, let’s create the entry point for the library and point to that in package.json— index.js:

if (process.env.NODE_ENV === "production") {  
module.exports = require("./lib/prod");
} else {
module.exports = require("./lib/dev");
}

This entry file will return either the optimized or the development version of the bundled library depending on the environment (this is how React itself does it). Next, in rollup.config.js we want to make sure that the compilation output is written to the right file, depending on the value of the global variable process.env.NODE_ENV. We can set that with rollup-plugin-replace:

const NODE_ENV = process.env.NODE_ENV || "development";
const outputFile = NODE_ENV === "production" ? "./lib/prod.js" : "./lib/dev.js";
export default {
...
output: {
file: outputFile,
...
},
plugins: [
replace({
"process.env.NODE_ENV": JSON.stringify(NODE_ENV)
}),
...
],
...
}

You might be wondering — wouldn’t it be simpler to just skip transforming styled-jsx with Babel and let client configuration handle it? That would simplify things a lot, because we wouldn’t have to worry about serving two different bundles, since the client would transform it into its target environment. While this is a valid concern, the reality is that the majority of apps don’t compile node_modules because this would drastically slow down the compilation time, and would potentially require complex build systems to account for every possible configuration for its dependencies. As library authors, we are responsible for transforming our code upfront; we should provide code that is ready-to-use.

Handy scripts

Let’s define some scripts to make our life easier:

"scripts": {  
"prepublishOnly": "npm run release",
"release": "npm run build:dev && npm run build:prod",
"build:prod": "NODE_ENV=production rollup -c",
"build:dev": "NODE_ENV=development rollup -c"
},

Calling npm run release will create lib/dev.js and lib/prod.js to be consumed by index.js. If you take a look these two files, there are some differences — one includes source-maps, and the other doesn’t. This will make a difference when your library gets bigger. The prepublishOnly script is a npm hook that will get called every time we run npm run publish. We want to make sure that we publish the latest-transpiled version of our package.

P.S if you are on Windows, you will need cross-env to set NODE_ENV.

Voilà! Everything is ready.

Summary

When you learn how to create configurations like this one, you’ll most likely figure out all the requirements in your own individual way. I wanted to point out a few things you need to keep in mind during that process.

I used to configure webpack when I wanted to create my own library, until someone tipped me off to give it a try with Rollup. I’m not telling you never to use webpack — it just seems like the latter is simpler and more lightweight for building packages. And React uses it, so I think there’s no point trying to convince you to give it a try!

Thanks to @giuseppeg for helping me out with this and my friend Sarah Kutz for the text review.