How to use lerna with React Native
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 watchFolder
in 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 variousnode_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 packagenode_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)