How to use lerna with React Native

Dushyant Bansal
4 min readJun 6, 2020

If you are using React Native for mobile, there’s a lot to gain from sharing codebase between mobile and web.

If your setup is straightforward and you just want to share one folder between web and mobile, you can refer to my previous post of sharing codebase.

However, you’ll see such setup in practice rarely. Most likely, you’ll have multiple packages or libraries in your monorepo and some of those will be shared across web and mobile.

Lerna is a great tool to manage your multiple JS/TS projects in a single codebase.

Using lerna for React Native is slightly tricky — one of the main reason being the lack of documentation and a working sample to play with.

Here is a sample ReactNative project working with lerna. — https://github.com/db42/lerna-with-react-native

Internal project structure of lerna-with-react-native:

lerna.json
packages/
- shared
- - components
- - Utils.ts, ....code
- WebApp
- - node_modules
- - ....code
- MobileApp
- - node_modules
- - src/
- - ...code, metro.config.js, rn-cli.config.js, etc...

Using Lerna to build React Native project

MobileApp has a dependency on shared package.

Notice the shared package in the list of dependencies: "shared" : "*" where shared in an internal package present in packages/shared folder.

MobileApp/Package.json :

{
...
"dependencies": {
"react": "16.11.0",
"react-native": "0.62.2",
"shared": "*"
},
....

MobileApp/App.js :

import { getVersion } from "shared";

Let’s try to build this project.

If you run lerna bootstrap (more info) inside lerna-with-react-native, it will create a symlink for all the dependencies on the local packages:

MobileApp
- node_modules
- - react-native/
- - shared -> ../../shared

Here Lerna has created a symlink for the shared package.

If you run metro bundler usingnpx react-native start , it will throw this error:

error: Error: Unable to resolve module `shared` from `App.js`: shared could not be found within the project.

To resolve this, we’ll have to update watchFolderin metro.config.js to include packages directory (folder where all the local packages reside).

MobileApp/Metro.config.js :

/**
* Metro configuration for React Native
* https://github.com/facebook/react-native
*
* @format
*/
const path = require('path');
const watchFolders = [
//Relative path to packages directory
path.resolve(__dirname + '/..')
];
];
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
watchFolders
};

Run metro bundler again and react-native won’t complain this time and will be able to resolve shared package successfully.

Advanced: Using lerna hoisting

tldr; Lerna hoist doesn’t work if you have native modules

Lerna gives you an option to hoist packages.

When an overall project is divided into more than one NPM package, this organizational improvement generally comes with a cost: the various packages often have many duplicate dependencies in their package.json files, and as a result hundreds or thousands of duplicated files in various node_modules directories. By making it easier to manage a project comprised of many NPM packages, Lerna can inadvertently exacerbate this problem.

Fortunately, Lerna also offers a feature to improve the situation — Lerna can reduce the time and space requirements for numerous copies of packages in development and build environments, by “hoisting” dependencies up to the topmost, Lerna-project-level node_modules directory instead.

What this means is that packages get installed in topmost node_modules directory. Local/internal packages continue to get installed (symlinked) in the projects node_modules .

lerna.json
node_modules
- react-native/
- react/
packages/
- MobileApp
- - node_modules
- - - shared --> ../../shared

You can see that react-native and react packages are installed in root node_modules instead of node_modules inside MobileApp .

Unfortunately, some tooling does not follow the module resolution spec closely, and instead assumes or requires that dependencies are present specifically in the local node_modules directory. To work around this, it is possible to symlink packages from their hoisted top-level location, to individual package node_modules directory. Lerna does not yet do this

But React Native expects many packages to be present inside MobileApp/node_modules e.g. various build scripts and source for iOS Pods.

To work around this, you’ll have to create a symlink packages inside MobileApp/node_modules which references to <root-dir>/node_modules/*.

lerna.jsonnode_modules
- react-native/
- react/
packages/
- MobileApp
- - node_modules
- - - react-native --> ../../../node_modules/react-native
- - - react --> ../../../node_modules/react
- - - shared --> ../../shared

You’ll also have to update watchFolders inside metro.config.js to include <root-dir>/node_modules .

/**
* Metro configuration for React Native
* https://github.com/facebook/react-native
*
* @format
*/
const path = require('path');
const watchFolders = [
path.resolve(__dirname + '/..'), //Relative path to packages directory
path.resolve(__dirname + '/../../node_modules') //Relative path to packages directory
];

Once you do this, your react native project will continue to run.

But this has a limitation. If you’re using native modules, you’ll encounter errors while building pods. (see https://github.com/db42/lerna-with-react-native/tree/hoist for a sample project with a native module)

--

--