DEV Community

Cover image for React Monorepo with NPM
Yuriy
Yuriy

Posted on • Updated on

React Monorepo with NPM

Motivation - Setup react monorepo project using NPM workspaces

πŸ‘‰ More about NPM Workspaces


Preferred structure

We're going to have next project structure:

.
└── root/
    β”œβ”€β”€ node_modules/
    β”œβ”€β”€ packages/
    β”‚   β”œβ”€β”€ app1/ (create-react-app)
    β”‚   β”‚   β”œβ”€β”€ src/
    β”‚   β”‚   └── package.json
    β”‚   └── common/ (shared component library)
    β”‚       └── components
    β”‚       └── utils
    β”‚       └── package.json
    β”œβ”€β”€ package.json
    └── package.lock.json
Enter fullscreen mode Exit fullscreen mode

Implementation details πŸ“š

Project init via npm workspaces

  • Step/1 - In the root project, create a package.json file with the following config.
{
  "name": "npm_workspaces",
  "private": "true",
  "workspaces": ["./packages/*"]
}
Enter fullscreen mode Exit fullscreen mode
  • Step/2 Create packages directory within the root folder

  • Step/3 Create app1

    • Navigate to the packages folder
    • Create new CRA project using following command npx create-react-app app1 --template typescript
  • Step/4 Once we have app1 let's do a few changes πŸ› 

    • In app1/package.json rename project name from app1 to @ng_speedster/app1. Why? Naming packages with @{domain}/{package-name} will group all the packages inside one common folder in node_modules. It allows for easier debugging if something some packages are not working properly or some else goes wrong. In my case, I have renamed the namekey in the app package as @ng_speedster/app1.
    • Move packages dependencies from app1/package.json to root/package.json. I'm doing that, cause I would like to have consistency between my react projects. In short I just want to have the same versions of react related dependencies:
  {
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.21",
    "@types/react": "^17.0.38",
    "@types/react-dom": "^17.0.11",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "typescript": "^4.5.4",
    "web-vitals": "^2.1.3"
  }
Enter fullscreen mode Exit fullscreen mode
  • Step/5 Create common package

    • Basically, this step repeats step 3 & 4;
    • Navigate to the packages folder
    • Create new CRA project using following command npx create-react-app common --template typescript
    • Move/merge packages dependencies from common/package.json to root/package.json.
  • Step/6

    • Remove node_modules from app1 and common projects
    • Navigate to the root folder and run npm install - It will install all the dependencies across all the packages into the root. You can then check the root node_modules folder to locate your package.

Projects is added, what's next?

Transpiling JSX into js from the shared component package

As we are not transpiling (converting JSX into createElements) from our shared components package, we will rely on cra to do the transpilation step. It makes the process a whole lot easier. To do so, we have to customize babel by using react-app-rewired and customize-crapackages. It allows you to customize webpack configuration without ejecting react-scripts.

  • Step/1 Installing and configuring react-app-rewired and customize-cra

    • Navigate to the root folder
    • npm workspace @ng_speedster/app add -D react-app-rewired customize-cra
    • In the app1/package.json make the next changes:
  {
    "scripts": {
      "start": "react-app-rewired start",
      "build": "react-app-rewired build",
      "test": "react-app-rewired test",
      "eject": "react-scripts eject"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Step/2 Override babel config by creating the config-overrides.js file in the app package.
  const path = require("path");
  const { override, babelInclude } = require("customize-cra");
  module.exports = function (config, env) {
    return Object.assign(
      config,
      override(
        babelInclude([
          /* transpile (converting to es5) code in src/ and shared component library */
          path.resolve("src"),
          path.resolve("../common"),
        ])
      )(config, env)
    );
  };
Enter fullscreen mode Exit fullscreen mode
  • It instructs babel to transpile all the files in the app's src directory and shared component package.

So that's it, now you can create component inside common/and import inside app package.

Image description

Overall

Honestly, to set up a monorepo project using npm workspaces is not that painful but requires
to dig into some areas:

Image description

  • Customize-cra takes advantage of react-app-rewired's config-overrides.js file. By importing customize-cra functions and exporting a few function calls wrapped in our override function, you can easily modify the underlying config objects (webpack, webpack-dev-server, babel, etc.) that make up create-react-app.

I didn't bring up, how to set up CI/CD builds and configure TS/JS tools - eslint/tslint etc, or how to manage multiple run-time builds, and many other things, therefore any
additional customization depends on your needs, hence, there are no CLI solutions like CRA from scratch etc...

See final source code here

Top comments (1)

Collapse
 
ivorobioff profile image
Igor Vorobiov

Thanks Yuriy for sharing it. It was very helful.