Mobile Development 15 min read

Monorepo Migration for React Native Projects: Practices, Tools, and Pitfalls

To eliminate duplicated checkout logic and cumbersome npm publishing, the team migrated multiple React Native apps into a Yarn‑workspace monorepo, configuring Metro watch folders, hoisting dependencies, and CI linting, while using git‑subtree for integration, achieving easier code reuse, debugging, and dependency management despite RN‑specific build complexities.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Monorepo Migration for React Native Projects: Practices, Tools, and Pitfalls

Background

Multiple React Native (RN) checkout scenes (page checkout, overlay checkout, customized checkout, etc.) exist in separate projects. The core logic such as product display, payment method display, and order creation is largely duplicated, leading to low efficiency when modifications or new features are needed.

Although publishing an npm package could reuse code, some components are hard to extract, and the process introduces debugging and release overhead. To improve code reuse and development efficiency, the team decided to adopt a monorepo structure that contains multiple projects in a single repository.

What is a Monorepo

A monorepo is a software development strategy that stores the code of multiple projects in a single repository, as opposed to the traditional multi‑repo approach where each project has its own repository. Notable open‑source projects such as Babel, React, and Vue use this strategy.

Problems with Multi‑Repo

When sharing code across two projects under a multi‑repo setup, developers typically publish an npm package. Any change to the package requires:

Modify the npm package and use npm link for debugging.

Publish a new version after debugging.

Upgrade both projects to the new version and release.

This workflow is cumbersome. In a monorepo, a shared workspace can be referenced directly by each project, eliminating the need for publishing, version management, and simplifying dependency handling.

The main advantages of a monorepo are:

Easy code reuse

Convenient debugging

Simplified dependency management

Drawbacks include larger repository size and more complex permission control.

Monorepo Tools

Common tools for implementing a monorepo are Lerna, Yarn workspace, and pnpm. Lerna is now deprecated, pnpm has issues with Metro’s symlink resolution, and Yarn workspace provides built‑in hoisting and soft‑link management. The team chose Yarn workspace for the RN projects.

Metro Build Process

Metro, the RN bundler, goes through three stages:

Resolution – builds a dependency graph using jest-haste-map .

Transformation – converts module code to a platform‑compatible format.

Serialization – serializes transformed modules into one or more bundles.

Project Transformation

The two RN projects were placed under a single repository with the following layout:

rn-mono
|-- apps
|   |-- app-a
|   |-- app-b
|-- package.json

The root package.json defines Yarn workspaces:

{
  ...
  "workspaces": {
    "packages": ["apps/*"]
  },
  "private": true
}

After running yarn install , shared dependencies are hoisted to the root node_modules . Each app can be started with:

yarn workspace app-a run dev

Because the RN CLI resides in the root, the dev script in app-a/package.json must reference the correct path:

{
  "scripts": {
    "dev": "node ./node_modules/react-native/local-cli/cli.js start"
  }
}

If the command is invoked via ./node_modules/.bin , the symlink from the root .bin resolves automatically.

Adjusting Metro Configuration

Metro’s default watcherFolders points to the project root, causing it to miss modules installed in the root node_modules . The configuration was updated:

// app-a/metro.config.js
const path = require('path');
module.exports = {
  watchFolders: [path.resolve(__dirname, '../../node_modules')]
};

After adding a common folder for shared components, the workspace and watchFolders were extended to include it:

// package.json
{
  ...
  "workspaces": {
    "packages": ["apps/*", "common"]
  },
  "private": true
}

// apps/app-a/metro.config.js
const path = require('path');
module.exports = {
  watchFolders: [
    path.resolve(__dirname, '../../node_modules'),
    path.resolve(__dirname, '../../common')
  ]
};

A simple Button component was added to common , and its dependencies were aligned with those of the apps:

{
  ...
  "dependencies": {
    "react": "16.8.6",
    "react-native": "0.60.5"
  }
}

Now apps can import the shared module like an npm package:

import common from 'common';

Dependency Hoisting

Hoisting ensures that identical dependencies are installed only once at the root, avoiding duplicate installations and naming conflicts in the dependency graph. The nohoist option can exclude specific packages, but doing so may cause jest-haste-map errors due to duplicate versions.

{
  "workspaces": {
    "packages": ["apps/*", "common"],
    "nohoist": ["**react**"]
  },
  "private": true
}

To keep versions consistent across packages, a lint script was created and integrated into GitLab CI:

// .gitlab-ci.yml
test-dev-version:
  stage: test
  before_script:
    - npm install --registry http://rnpm.hz.netease.com
  script:
    - npm run depVerLint
  only:
    changes:
      - "package.json"
      - "packages/**/package.json"

Migration with Git Subtree

When migrating actively developed projects, git subtree can embed an existing repository as a subdirectory, allowing easy updates:

# Add
git subtree add --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash

# Update
git subtree pull --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash

Build Integration

Because the build machines do not support Yarn directly, a wrapper script was added. The script invokes Yarn workspace commands for each app:

## scripts/build.sh
PLATFORM=$1
PROJECT=$2
EXEC_PARAMS=${@:2}
YARN="$PWD/node_modules/.bin/yarn"

echo "start yarn install"
${YARN} cache clean
${YARN} install

echo "start build"
${YARN} workspace ${PROJECT} run build:${PLATFORM} ${EXEC_PARAMS}

The root package.json defines the build script:

{
  ...
  "workspaces": {"packages": ["apps/*"]},
  "private": true,
  "scripts": {"build": "./script/build.sh"}
}

To build app-a for iOS:

npm run build ios app-a
# Executes: yarn workspace app-a run build:ios

Conclusion

The monorepo transformation for React Native projects enables efficient code reuse, easier debugging, and streamlined dependency management. While the approach works well for H5 projects, RN’s unique build system introduces challenges that require careful configuration of Metro, hoisting, and CI checks. The documented experience and scripts can serve as a reference for similar migrations.

Monorepodependency managementcode reuseReact NativeMetroYarn Workspace
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.