Sharing UI Components with Lerna and Yarn Workspaces

Naresh Bhatia
Naresh Bhatia
Published in
7 min readJun 12, 2018

Today’s popular front-end frameworks like Angular and React allow us to create rich web applications using modular, reusable components. Most of the time we get away with reusing components that someone else wrote. Angular Material, Material-UI and Glamorous are just a few examples of the many off-the-shelf component libraries that allow us to build rich web applications with minimal effort. We generally don’t think about writing shared reusable components until we have to write moderately complex applications. In my case, our team was tasked to write a set of reusable components so that they can be used to quickly compose a suite of complex financial applications. I started thinking about the best way to create these components.

Motivation: Faster Development Cycle

Imagine that you were writing an online shopping app. There’s a page in this app that shows the list of orders. The OrderList component uses the List component from the Material-UI library. Your component hierarchy might look like this:

Let’s see how these components may be organized in your repo:

Your repo has a src folder that contains the code you wrote. You also have a node_modules folder that contains reusable components installed from npm.

Now imagine that you needed to package OrderList as a independent component that could be reused across several applications. How might you do that? Here’s one way:

We have created a new repo called myshared. This repo contains the OrderList component along with its dependencies — in this case the List component from Material-UI. We have published the myshared repo to npm and installed it in myapp. This allows us to use OrderList in myapp and any other app. Yeah! But do you see an issue with this approach?

Of course! For every change in OrderList, we must publish myshared to npm and reinstall it in myapp. This is extremely frustrating. How can we speed this up?

Note that I have glossed over an important step — before publishing to npm, we should unit test our component to make sure it works as expected. We could use testing frameworks such as Jest or Jasmine to do this. We could also use Storybook to visualize different states of our component and develop it interactively.

Lerna and Workspaces - Package Management Made Easy

Lerna and Yarn Workspaces give us the ability to build libraries and apps in a single repo (a.k.a. monorepo) without forcing us to publish to npm or other registries. We can go through code-test-debug cycles much faster by sharing components locally. Of course, we still have the option of publishing to npm at any time. Let’s see how this works.

Lerna & Yarn Workspaces expect a folder called packages in your repo. A package is simply a “mini-repo” that can be versioned, built and published independently. The monorepo shown in the diagram below has two packages

  • myshared: this is a library of reusable components
  • myapp: an app that uses the components in myshared

We can have any number of packages inside a monorepo, with complex dependencies between each other. The secret sauce behind managing these dependencies is the concept of symlinks. In computing, a symlink (short for Symbolic Link) is a file that references another file or directory and makes it look like the referenced file or directory is at the original file’s location. Looking at the diagram above, there is a symlink called myshared under myapp/node_modules that references the myshared package under packages. The beauty behind Lerna and Yarn Workspaces is that these tools can find package dependencies by analyzing the package.json files under each package. After determining these dependencies, they create symlinks that make each package think that their dependencies have been installed under their own node-modules folder! Once these links are established, we can build any package without publishing dependent packages to a registry. This is pretty neat!

Now you may be thinking, why do we need both Lerna and Yarn Workspaces? What roles do they each play in this workflow? Quite frankly, there is a big overlap between the functionalities offered by the two tools. Both are able to analyze package.json files and create the necessary symlinks. However, Lerna falls short in some important use cases — as I found out the hard way. Turns out that libraries like React and MobX get very upset if an app loads multiple copies of these libraries (see this React warning and this MobX issue for details). Unfortunately, Lerna does exactly that even if we carefully specifying dependencies, dev dependencies and peer dependencies. On the other hand, Yarn Workspaces has mastered this task. It automatically detects such dependencies and includes the libraries only once in the final app. It’s the best tool I know to manage package dependencies in a monorepo.

So what purpose does Lerna serve? Well, Lerna provides the high-level commands to optimize the management of multiple packages. For example, with one Lerna command you can iterate through all the packages, running a series of operations (such as linting, testing and building) on each package. It compliments Yarn Workspaces in managing your monorepo.

Let’s give it a spin

Now that we understand what Lerna and Yarn Workspaces do, let’s try them out first-hand. I will make it easy for you — just clone my sample repo from Github. This repo contains vanilla JavaScript code, no fancy React or Angular components, but it is enough to understand the fundamentals. We have two packages:

  • temp-utils: a package containing temperature conversion utilities. This package has an external dependency on lodash (you can see this in the package.json file).
  • weather-app: a command-line app that uses the temp-utils package (as specified in its package.json file).

The key to setting up Lerna and Yarn Workspaces is the lerna.json configuration file in the root directory:

{
"lerna": "2.11.0",
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true
}

The configuration basically tells Lerna to use independent versions for each package and to enable integration with Yarn Workspaces. That’s it! We are now ready to roll. Open a command line shell and set you directory to the root of the monorepo. Run the following command:

$ yarn

This allows yarn to analyze all the packages in your monorepo. It will download external dependencies (like lodash) from npm and create symlinks for internal dependencies. You will notice that yarn does not create node_modules directories in either of your packages — this is not the usual case. For this repo, Yarn has hoisted all the dependencies to its root. If you look inside lerna-workspaces-concepts/node_modules you will find the symlink to temp-utils.

Now that Yarn has prepared the repository with symlinks, you can build the temp-utils library and the weather-app, all in one shot — thanks to Lerna! Execute the following command in the root directory:

$ yarn build

This command effectively runs the following Lerna command (see package.json). The Lerna command compiles the source folder in all packages using babel.

$ lerna exec -- babel src -d dist --ignore test.js

Notice how Lerna saved you time by going through each package and building it for you. No need to do this manually! Now you are ready to run your final app. Execute the following command:

$ yarn start

This effectively runs

$ node packages/weather-app/dist/weather-app

You should see the following output:

100 degrees C = 212 degrees F
212 degrees F = 100 degrees C
average of [10, 20, 30] = 20

This simple example shows the power of Lerna and Yarn Workspaces.

Starter Templates

Now that you understand the basics of Lerna and Yarn Workspaces, you should be able to create more complex monorepos to build reusable UI libraries and apps. Kick-start your monorepo using one of these starter templates:

  1. React ES6 template
  2. React TypeScript template
  3. React, MobX, Material-UI Monorepo using TypeScript

TL;DR

Lerna and Yarn Workspaces give us the ability to build libraries and apps in a single repo without forcing us to publish to npm or other registries. We can go through code-test-debug cycles much faster by sharing components locally.

Yarn Workspaces finds package dependencies by analyzing the package.json files under each package and creates symlinks to satisfy internal dependencies.

Lerna provides high-level commands to optimize the management of multiple packages.

Lerna and Yarn Workspaces together improve the developer experience of managing multiple packages in a monorepo.

--

--