Sharing React Components with Lerna
By Andrew Cashmore

Sharing React Components with Lerna

All the code for this article can be found here

Lerna - Component Sharing in React done right!

At nearForm, we’ve mostly become accustomed to creating well architected applications with components in React. We’ve been doing it for quite some time, but we see the same stumbling block over and over: how do you effectively share front-end components across various projects within an organisation?

Where do you start? Do you create a repository for each component? One repo for everything? A toolkit?

For me, this is where the monorepo approach steps in. It’s a convenient way to manage your small distributable pieces of code without the overhead of many repositories and the setup involved with that. That’s not to say that a monorepo doesn’t have its own drawbacks but we’re able to alleviate the majority of that pain with a tool called Lerna.

What is Lerna?

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

Okay but what does that really mean?

In short, we have one repository with all our code. We setup a ‘containing’ repository for all our ‘packages’ to live in which allows us to manage a single setup at the top. By setup I mean testing, linting, build processes, continuous integration and the like.

This allows our packages to stay extremely small and manageable and any change in our setup can just be reflected in a single place.

Getting started with Lerna

Installing Lerna

To get started let’s initialise Lerna and NPM.

npm install -g lernagit init lerna-repo && cd lerna-repo

Now let’s initialise Lerna with lerna init --independent. This will create a lerna.json and package.json file. In this case we’re using Lerna in independent mode which allows us to individually manage the version of each package.

There is also a default mode called fixed/locked which uses a single version number in the lerna.json file to publish packages when they’ve been updated, this method is what babel use in their Lerna setup and is less involved than manually bumping versions for each changed package.

The mode you choose will come down to personal preference and what you’re using the Monorepo for. Independent mode allows me to control each package as I choose and prevents me releasing major versions unnecessarily, this might not be a concern depending on what you’re publishing.

Use this if you want to automatically tie all package versions together. One issue with this approach is that a major change in any package will result in all packages having a new major version.

Basic React Setup

We’re going to use Lerna as a home for a set of React components we’d like to share. For this we’re going to add a few common libraries like Babel, Storybook and Jest in, a copy of our package.json is below:

{
  "devDependencies": {
    "@storybook/react": "^3.0.0-alpha.4",
    "babel-cli": "^6.24.1",
    "babel-jest": "^20.0.3",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
    "babel-preset-env": "^1.5.1",
    "babel-preset-react": "^6.24.1",
    "enzyme": "^2.8.2",
    "glob-loader": "^0.3.0",
    "jest": "^20.0.0",
    "lerna": "^2.0.0-rc.4",
    "react": "^15.5.4",
    "react-dom": "^15.6.1",
    "react-test-renderer": "^15.6.1"
  },
  "jest": {
    "resetModules": true,
    "testMatch": [
      "**/?(*.)(spec).js?(x)"
    ],
    "testPathIgnorePatterns": [
      "<rootDir>node_modules/"
    ]
  }
}

Now we add in a .babelrc file:

{
  "presets": ["env", "react"],
  "env": {
    "test": {
      "plugins": [
        "transform-es2015-modules-commonjs"
      ]
    }
  }
}

We’re now going to add some basic Storybook configuration to allow it to ‘find’ our packaged stories using glob-loader. Create a directory called .storybook. Within that we need to create two files:

config.js

import React from 'react'
import { configure } from '@storybook/react'

function loadStories () {
  require('glob-loader!./stories.pattern')
}

configure(loadStories, module)

stories.pattern

../packages/**/*.story.js
Our First Package

We’re ready to start putting packages into our Lerna repo. Start by creating a packages directory which is the default Lerna directory. This can be changed in the lerna.json configuration if you want a different name for your packages.

To save time, clone down our example repository. Open the examples directory and copy our ‘test-package’ into your packages directory. If you’ve got some extra time, feel free to create your own package similar to ours!

The example package contains a small React component with a Jest test and a Storybook story.

Bootstrapping

So an obvious drawback of having all our packages in one repository is having to go into each package and npm install as the Monorepo grows this would quickly become unmanageable and time consuming. Lerna has various built in commands to ease the drawbacks and bootstrap is one of them!

Simply run lerna bootstrap at the top level and Lerna will automatically install the required dependencies for each package as well as resolving internal dependencies by creating symlinks.

Storybook, Jest

Now that we’re setup, have an example package to test with, we can start to see the benefits of Lerna. Lets add some scripts into our package.json:

"start": "NODE_ENV=development start-storybook -p 9001 -c .storybook","test": "jest --verbose"

Once you’ve added the scripts, try running each of them! Storybook should automatically detect any stories within our packages allowing us to have a ‘live’ documentation of our components for everyone to see. Equally Jest will do the same, looking in our packages for any tests that need running, we can even add coverage with a few lines of code.

Building

Another great feature of Lerna is the exec command. Essentially Lerna allows us run a command within each of our packages. In our case this is really useful for building our components ready to be published onto npm.

Here’s an example of how we’d go about compiling each of our React components:

lerna exec --parallel -- babel src -d dist --ignore spec.js,story.js

Demonstrating this on a single package is probably fairly underwhelming but as the Monorepo grows you can easily see the benefit of being able to do this in a single line of code, again if your build process changes it’s in one place.

But I want a different build process for each package? - Simple, just use lerna exec npm run build and define a custom build script within each repositories package.json!

Publishing

So, we’ve got a great setup that’s all managed in one place, we have a great developer environment but the final piece of the puzzle is using publish.

Publish is another great time and hassle saver, it automatically pushes up all of your packages to Git and NPM, specifically Lerna documents it does the following:

  • Run the equivalent of lerna update to determine which packages need to be published.
  • If necessary, increment the version key in lerna.json.
  • Update the package.json of all updated packages to their new versions.
  • Update all dependencies of the updated packages with the new versions, specified with a caret (^).
  • Create a new git commit and tag for the new version.
  • Publish updated packages to npm.

Summary

If the post hadn’t already given it away, I’m really onboard with Lerna. I see the huge benefits of having a Monorepo and all our code in a single place and, from a developer perspective, it feels like a great step towards making our own lives easier when it comes to maintaining everything.

The Monorepo architecture combined with Lerna resolves the majority, if not all, of the drawbacks that come with maintaining and using a Monorepo and it’s something I’m keen to see become more commonplace.

Subscribe to our monthly newsletter!
join the discussion