How to develop React Native in workspaces

Edvard Chen
4 min readMay 11, 2021

The article would help you setup to develop react native project in mono repo.

Workspaces

You can see this link to get the concept.

We use yarn workspaces in this guide.

What’s the problem

Well, when resolving modules, react-native-cli doesn't search modules in parent node_modules folder outside projectRoot (that directory you run the command by default), unlike native builtin require(..) of Node.js or webpack.

For example,

├── node_modules
│ ├── A -> packages/A
│ └── B -> packages/B
└── packages
├── A // A import B
└── B

If we try to build bundle in packages/A folder, it would fail because it cannot find the module B

So, we need to some work to do.

Environments

  • yarn@1.22.5
  • Node.js@14.16.0
  • react-native@0.59.9

Possible approaches

Before starting, there are several things we should keep in mind.

  • The react-native packager would crawl those files in rootProject and watchFolders.
  • It only allows one copy of react-native to be included, otherwise the duplicate module issue would come out:
Loading dependency graph...jest-haste-map: @providesModule naming collision:   Duplicate module name: react-nativeLoading dependency graph...jest-haste-map: @providesModule naming collision:   Duplicate module name: react-native
  • It ignores symlinks, no matter we enable watchman or not
  • It don’t search module in parent node_modules

Approach 1. Don’t hoist any dependency

First thought is to prevent hoisting any dependencies needed by A regardless of their level, including package B. So we would get all files in the packages/A folder.

After yarn install, we got:

├── lerna.json
├── package.json
├── packages
│ ├── A
│ │ ├── node_modules
│ │ │ └── B -> ../../B
│ │ ├── index.js
│ │ └── package.json
│ └── B
│ ├── index.js
│ └── package.json
└── yarn.lock

IMPORTANT NOTICE

If you somehow ran into the issue Error: ENOENT: no such file or directory, lstat … after running yarn install and couldn't resolve it unfortunately, then you should say goodbye to this approach

Why add ./packages into watchFolders ? Because the packager ignore symlink, do that to avoid the cannot-resolve-module issue:

error: bundling failed: Error: Unable to resolve module `B` from `.../packages/A/index.js`: Module `B` does not exist in the Haste module map

Sure. You can work around this with patch. And the bundler cannot watch the files’ changing and rebuild automatically

Last thing you should notice, if B/node_modules/react-native exist , then you should ignore the one under B. The config would be more complicated:

  • blacklistRE : ignore other react-native to avoid the duplicate module issue
  • extraNodeModules : tells resolver to resolve react-native for files in B folder to A/node_modules/react-native, since we already ignored that one in B/node_modules

Done. You can start your work.

Approach 2. Hoist anything except react-native

  • watchFolders: notice that we add ROOT folder into watchFolders instead.

So the keynote is

  • Make other workspaces crawlable through adding them into watchFolder
  • Make sure only one copy of react-native module is crawled

Another little issue of iOS production bundling

What happens when we build the react-native bundle?

  1. Setup the packager
  2. Build the iOS/android JavaScript bundle
  3. Copy the assets (images) used by the bundle above to specific folder

The processed of step 3 on iOS and android are different.

  • For iOS, it would keep the relative paths of assets to project root (cwd).
  • For android, all imported assets would be placed in one same folder.

For example, we imported an image in B and an image in A for comparison:

├── packages
│ ├── deep
│ │ └── A
│ │ ├── node_modules
│ │ │ └── B -> ../../B
│ │ ├── index.js
│ │ └── package.json
│ └── B
│ ├── index.js
│ └── package.json
└── yarn.lock

after running the following command in packages/deep/A

react-native bundle --platform ios --entry-file ./index.js --bundle-output dist/index.ios.js --assets-dest dist

the result would be strange:

├── B
│ └── assets
│ └── bar.png
└── dist
├── assets
│ └── assets
│ └── foo.png
└── index.ios.js

The assets used by module B are placed outside the dist folder. So we should change the behavior, by adding our custom asset plugin

Then, the output would be:

dist
├── assets
│ ├── _
│ │ └── _
│ │ └── B
│ │ └── assets
│ │ └── bar.png
│ └── assets
│ └── foo.png
└── index.ios.js

Everything is OK now.

--

--