Simplify your monorepo with npm 7 workspaces
Tags: react, npm, devops, monorepo
Published: 2020-10-31 00:56:39

[![Hot reloading for UI React component](https://dev-to-uploads.s3.amazonaws.com/i/52bowb84cy732teck6ky.gif)](https://monorepo-anim.wolnik.co.uk/)

This month npm has [released a major version](https://blog.npmjs.org/post/631877012766785536/release-v700) of their package manager **npm 7**. It shipped with support for workspaces.

Why is it big news? Because npm is the only package manager that comes bundled with every NodeJS. To use *yarn* or *pnpm* you have to take an additional step and install them first.

Read on and you'll find out how to use npm 7 workspaces in a real-world scenario and learn that using workspaces the npm's way is very different to yarn's.

## Monorepo use cases
A monorepo is a term describing a single git repository that contains many projects.

The most common reason to set up a monorepo is to streamline work within a dev team that maintains multiple apps that are using a shared piece of code, for example a common User Interface library.

Imagine a team that develops two React apps that shares some common UI elements like inputs, selectors, accordions, etc. It would be nice to extract that UI in form of React components and prepare building blocks that are ready to use for all members of the team.

Apart from that it's just more convenient to have all your source files opened in a single IDE instance. You can jump from project to project without switching windows on your desktop.

## I just want that nice button in my app too
Let's say I want to build two independent React apps called `app1` and `app2` that will use a common component from a common UI library called `ui`. And I want both apps to hot reload whenever I edit a file inside the UI library.

By independent I mean that `app1` doesn't know anything about `app2` and vice-versa.

Below is a setup that is compatible with npm 7 workspaces.

![Monorepo structure](https://dev-to-uploads.s3.amazonaws.com/i/hcf8f0qc0s6gzin0fic5.png)

## Defining workspaces in npm 7

> *I am prefixing all packages' names with @xyz so they will be distinctive from the official npm registry. Just change it to yours.*

This is the most crucial part of the whole setup. Insert below inside your root folder's `package.json` to set up a monorepo.

```json
{
    "name": "@xyz/monorepo",
    "private": true,
    "version": "1.0.0",
    "workspaces": [
        "./common/*"
    ]
}
```

The new `"workspaces"` property lets npm know that I want to track any packages inside `./common` folder and automatically symlink them in the root's `node_modules` when I run `npm install`.

From now on whenever our React apps will use `import Foo from "@xyz/ui"` the NodeJS will find it in `./node_modules/common/@xyz/ui` that points to `./common/ui` folder that contains our library. Perfect! No need for `npm link` anymore with the workspaces.

Without workspaces the React app would complain that it cannot find a module named `@xyz/ui` and would start looking for it in the npm official registry.

## Take the first step

To test our setup let's share a text from the `ui` library and import that string into our React app.

Create the common UI library's `package.json`:

```json
{
    "name": "@xyz/ui",
    "version": "1.0.0",
    "private": true,
    "main": "index.js"
}
```

and `index.js` file that will export a string:

```javascript
const version = "This comes from UI! 1.0.0"

export default version;
```

Time to import that string into our apps.

> To quickly create React apps inside our monorepo. Just use the newly released Create React App 4:
```bash
mkdir apps
cd apps
npx create-react-app app1
npx create-react-app app2
```

Now you'd think that we need to add our `ui` library to the app. In yarn it would look like below:

```shell
yarn workspace app1 add @xyz/ui
```

But with npm we don't need to add any dependency at all.

Just go to your `App.js` file in both **app1** and *app2* apps and add the below code to display a string from our UI library:

```javascript
...
import testString from "@xyz/ui"; 
...
    <span>{testString}</span>
...
```

To test it use the below commands:

```
# create a symlink to the @xyz/ui in the root folder
npm install
# go to the app's folder
cd apps/app1
# For CRA 4 you may need to add SKIP_PREFLIGHT_CHECK=true to .env file
# And use the --legacy-peer-deps flag as many packages hasn't been updated yet to officially support React 17
npm install --legacy-peer-deps
npm run start
```

and from another terminal window:

```
cd apps/app2
npm install
npm run start
```

You'll see the `This comes from UI! 1.0.0` text rendered inside both of your React apps!

## Export React JSX components
Now if you'd try to export a JSX component the React apps will complain that they cannot parse the JSX. You need to transpile JSX code from the common UI package first.

You can use a basic Webpack 5 setup:

`common/ui/package.json`
```json
{
    "name": "@xyz/ui",
    "version": "0.2.0",
    "private": true,
    "module": "build/ui.bundle.min.js", # Changed main to module
    "scripts": {
        "build": "webpack --config webpack.prod.js",
        "build-watch": "webpack --config webpack.prod.js --watch",
    },
    ... # webpack 5 dependencies
}
```

`common/ui/babel.config.js`
```javascript
module.exports = {
  presets: [
    [
      "@babel/preset-react",
      {
        targets: {
          node: "current",
        },
      },
    ],
  ],
};
```

`common/ui/webpack.prod.js`
```javascript
const path = require("path");

module.exports = {
  entry: {
    index: { import: "./src/index.js" }
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      },
    ],
  },
  output: {
    filename: "ui.bundle.min.js",
    path: path.resolve(__dirname, "build"),
    // Below two important lines!
    library: 'xyzUI',
    libraryTarget: 'umd'
  },
};
```

Our simple component:

`common/ui/src/index.js`
```javascript
import React from "react";

const UIExample = ({ text = "" }) => {
  return (
    <div>
      <h1>Shared UI library {text}</h1>
    </div>
  );
};

export default UIExample;
```

Import the `UIExample` component inside your React app using below:

`apps/app1/src/App.js`
```javascript
...
import UIExample from "@xyz/ui";
...
    <div>
        <UIExample text="from app1" />
    </div>
...
```

Make sure the UI library is transpiled on every code change:

```
cd common/ui
npm run build-watch
```

Run the app1 in a separate terminal window and take notice that whenever you edit the UI component the webpack dev server will automatically reload it with the newest version thanks to the webpack watch running in the background.

```
cd apps/app1
npm run start
```

## Demo

Below I'm editing the common UI component `UIElement` and upon saving both React apps are automatically refreshed with the updated component:

![Hot reload](https://dev-to-uploads.s3.amazonaws.com/i/tbu8b7hujyin5e1h2uip.gif)

## Summary

With the newest npm 7 and its support of workspaces it is now possible to have a monorepo without a need of any external tools like `@react-workspaces` or `nx`.

**Just remember that npm has a different philosophy than yarn. For example you cannot run a script inside a workspace from the monorepo's root folder.**

Recognize also that `@xyz/app1` and `@xyz/app2` weren't defined in the monorepo's package.json `workspaces` property. Only the modules that will be exported need to be there (`@xyz/ui`).

**The npm 7 workspaces are mainly providing discovery for the modules.** I wish it had been emphasised in release notes and that the npm's help examples were a bit more complex. I hope this article fills this gap for the time being.

## Bonus
Check out my [gif-css-animation-monorepo](https://github.com/limal/gif-css-animation-monorepo) repository that shows how I made the animation for this article using [an HTML page](https://monorepo-anim.wolnik.co.uk/).