Setting up React Native Monorepo with Yarn Workspaces
Tags: reactnative, appdevelopment, monorepo, guide
Published: 2022-10-14 19:10:04

by Oskar Kwaśniewski

## What is a monorepo?
A monorepo is a single repository that holds a multitude of projects with all their code and assets. While the projects can be related, they can be used independently by different teams. 

Working with monorepos is very useful, especially when developing big and complex applications like [super apps](https://www.callstack.com/blog/why-have-a-super-app). Monorepos enable sharing the logic between a web app and a mobile app, for example.

In this article, we’re going to set up a basic structure for a monorepo in React Native.

## Why use monorepo in React Native?
Thanks to react-native-web we can share every component so there’s no need to duplicate the code many times. This means easier dependency management, shorter CI times, and better collaboration between teams since the code can be shared.

The main advantage though is the possibility of sharing packages between React and React Native. Most of the time we decide to share only business logic, but thanks to React Native Web, we can also share some of the UI components.

## Setting up Yarn workspaces
While setting up a monorepo, we have two options: we can either hoist the packages to the root level or prevent them from hoisting. Yarn workspaces have an option named nohoist which allows us to specify packages that aren’t hoisted. It depends on your use case but most of the time **it’s better to hoist packages to the root level**.

Alternatively, you can use [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces), it _should_ work the same.

## Why it’s better to avoid using nohoist
Preventing packages from hoisting takes back the main advantage of monorepos which is sharing `node_modules` between repositories. When nohoist option is turned on, most of the time packages are duplicated inside the root level `node_modules` and inside the project level `node_modules`. Downloading packages multiple times can lead to longer CI/CD runs and higher costs.

## Rearranging the project structure
In order to set up yarn workspaces, we need to restructure our project to suit the structure below. Projects need to be divided into separate folders and we should have root level `package.json`.

```
your-project
	packages
		web // <- React app
		mobile // <- React Native app
		shared // <- Shared (includes files shared between mobile/web)
```
[plain1.txt](https://gist.github.com/callstack-bot/153f4e4e15791065fd01dd57cb426730#file-plain1-txt) hosted with ❤ by GitHub

Next, you should create a package.json file in the root directory of our project with this command: `$ yarn init -y` 

It should look like this:

```
{
  "private": "true",
  "name": "example-app",
  "version": "1.0.0",
  "workspaces": [
    "packages/*"
  ]
}
```
[codesnippet1.json](https://gist.github.com/callstack-bot/00a07fe6d69f1d9fa9270db86bc3a2a4#file-codesnippet1-json) hosted with ❤ by GitHub

Note that private: true is required because workspaces are not meant to be published.

In the workspaces section, we define a **work tree**. We can pass there an **array of glob patterns that should be used to locate the workspaces**. So in the example above, every folder inside packages is defined as a workspace.

## Creating a React Native project
Use command below to create a React Native project inside the `packages` folder:

```
$ npx react-native init ExampleApp --directory mobile --template react-native-template-typescript
```
[plain2.txt](https://gist.github.com/callstack-bot/f666e8c6ada815b30444b70e6e1e4e19#file-plain2-txt) hosted with ❤ by GitHub

In order for our app to work in a monorepo structure, we need to make sure that config files are pointing to the root level `node_modules` due to package hoisting.

## iOS setup

```
Change paths at the top of your Podfile
- require_relative '../node_modules/react-native/scripts/react_native_pods'
- require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
+ require_relative '../../../node_modules/react-native/scripts/react_native_pods'
+ require_relative '../../../node_modules/@react-native-community/cli-platform-ios/native_modules'

target 'ExampleApp' do
...
```
[path1.diff](https://gist.github.com/callstack-bot/b6f07259a08e8af8f4b678266c724fd4#file-path1-diff) hosted with ❤ by GitHub

Regenerate Pods, by entering the command below in the packages/mobile folder:

```
$ cd ios && pod install
```
[plain3.txt](https://gist.github.com/callstack-bot/dc54172d5b9e2fb78e96b8c0ac95723c#file-plain3-txt) hosted with ❤ by GitHub

Next open Xcode and inside Project settings > Build Phases open “Bundle React Native code and images”

Change the path of the script:

```
set -e

- WITH_ENVIRONMENT="../../node_modules/react-native/scripts/xcode/with-environment.sh"
- REACT_NATIVE_XCODE="../../node_modules/react-native/scripts/react-native-xcode.sh"
+ WITH_ENVIRONMENT="../../../node_modules/react-native/scripts/xcode/with-environment.sh"
+ REACT_NATIVE_XCODE="../../../node_modules/react-native/scripts/react-native-xcode.sh"

/bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE" 
```

[path2.diff](https://gist.github.com/callstack-bot/47fa581379143d26ffb8b941d97f6969#file-path2-diff) hosted with ❤ by GitHub

It should look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5uy6fqsg2hbm1qom523.png)

## Android setup
**React Native ≥ 0.71**

Uncomment the line with `reactNativeDir` and point it to root `node_modules`

```
// packages/mobile/android/app/build.gradle

react { 
+  hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
}
```
[android 1.diff](https://gist.github.com/callstack-bot/959430f8f3dfb5f0ada39058e732c16e#file-android-1-diff) hosted with ❤ by GitHub

In top-level `build.gradle` add this:

```
allprojects {
    project.pluginManager.withPlugin("com.facebook.react") {
        react {
            reactNativeDir = rootProject.file("../../../node_modules/react-native/")
            codegenDir = rootProject.file("../../../node_modules/react-native-codegen/")
        }
    }
}
```
[android 2.shell](https://gist.github.com/callstack-bot/394a81925dab92be914fdfedac3d6bf0#file-android-2-shell) hosted with ❤ by GitHub

and inside: `packages/mobile/android/settings.gradle`

```
+ apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
+ includeBuild('../../../node_modules/react-native-gradle-plugin')
```
[android 3.diff](https://gist.github.com/callstack-bot/31ebc8fe1081b3d18d6dda7c5460f946#file-android-3-diff) hosted with ❤ by GitHub
‍
**React Native ≤ 0.71**

Fix paths inside `packages/mobile/android/build.gradle`
Change url of the React Native Android binaries, and Android JSC in the `allprojects` section:

```
allprojects {
    repositories {
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
-            url("$rootDir/../node_modules/react-native/android")
+            url("$rootDir../../../../node_modules/react-native/android")
        }
        maven {
            // Android JSC is installed from npm
-            url("$rootDir/../node_modules/jsc-android/dist")
+            url("$rootDir../../../../node_modules/jsc-android/dist")
        }
				...
    }
}
```
[code snippet2.diff](https://gist.github.com/callstack-bot/0f6d4ce0be60e41092ed684831768865#file-code-snippet2-diff) hosted with ❤ by GitHub

Fix `packages/mobile/android/settings.gradle`
Here we also need to change paths to the root `node_modules` folder.

```
rootProject.name = 'example-app'
- apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
- includeBuild('../node_modules/react-native-gradle-plugin')
+ apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
+ includeBuild('../../../node_modules/react-native-gradle-plugin')
include ':app'
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
    include(":ReactAndroid")
-   project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
+   project(":ReactAndroid").projectDir = file('../../../node_modules/react-native/ReactAndroid')
    include(":ReactAndroid:hermes-engine")
-   project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine')
+   project(":ReactAndroid:hermes-engine").projectDir = file('../../../node_modules/react-native/ReactAndroid/hermes-engine')
}
```
[path3.diff](https://gist.github.com/callstack-bot/912dc87a0fcaf5f3b3268ce802aea09d#file-path3-diff) hosted with ❤ by GitHub

Next go to `packages/mobile/android/app/build.gradle`
Inside the `project.ext.react` we need to add property `cliPath` so it will override the default location of the cli.

```
project.ext.react = [
+    cliPath: "../../../../node_modules/react-native/cli.js",
]
```
[plain4.txt](https://gist.github.com/callstack-bot/a138c380342986006c11453c81fa450a#file-plain4-txt) hosted with ❤ by GitHub

In the same file, change this line: `apply from: "../../node_modules/react-native/react.gradle"` to point to root level `node_modules` as well.

```
- apply from: "../../node_modules/react-native/react.gradle"
+ apply from: "../../../../node_modules/react-native/react.gradle"
```
[path4.diff](https://gist.github.com/callstack-bot/869f2e7ac507c6dbfb753454eccf2cc7#file-path4-diff) hosted with ❤ by GitHub

And also change: inside `packages/mobile/android/app/build.gradle`

```
- apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
+ apply from: file("../../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
```
[path5.diff](https://gist.github.com/callstack-bot/c49912a600849eaaaa91fb3a0c488707#file-path5-diff) hosted with ❤ by GitHub

If you are using or planning to use the new architecture, we also need to change a few lines in the `isNewArchitectureEnabled()` if statement**.**

```
if (isNewArchitectureEnabled()) {
            // We configure the NDK build only if you decide to opt-in for the New Architecture.
            externalNativeBuild {
                ndkBuild {
                    arguments "APP_PLATFORM=android-21",
                        "APP_STL=c++_shared",
                        "NDK_TOOLCHAIN_VERSION=clang",
                        "GENERATED_SRC_DIR=$buildDir/generated/source",
                        "PROJECT_BUILD_DIR=$buildDir",
-                       "REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
-                       "REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
-                       "NODE_MODULES_DIR=$rootDir/../node_modules"
+                       "REACT_ANDROID_DIR=../../node_modules/react-native/ReactAndroid",
+                       "REACT_ANDROID_BUILD_DIR=../../node_modules/react-native/ReactAndroid/build",
+                       "NODE_MODULES_DIR=../../node_modules"
                    cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
                    cppFlags "-std=c++17"
                    // Make sure this target name is the same you specify inside the
                    // src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
                    targets "okwasniewskionboarding_appmodules"
                }
            }
```
[code snippet 3.diff](https://gist.github.com/callstack-bot/8f6a7f101afe659c4b4d74f7d8f26531#file-code-snippet-3-diff) hosted with ❤ by GitHub

## Configure Metro
We’re almost done with setting up the project. The last thing in the React Native app is to add `watchFolders` so metro knows where the linked `node_modules` are. The shared modules are symlinked by yarn, and since [metro doesn’t follow symlinks](https://github.com/facebook/metro/issues/1) we need to explicitly say it where the linked `node_modules` are.

```
const path = require("path");

// packages/mobile/metro.config.js
module.exports = {
  watchFolders: [
    path.resolve(__dirname, '../../node_modules')
  ],
};
```
[code snippet4.javascript](https://gist.github.com/callstack-bot/36f0d1e17290db96872b9ce62eed73fa#file-code-snippet4-javascript) hosted with ❤ by GitHub

## Setting up a shared package
To keep it simple, we’re going to share only one function across React Native and React.

Inside the `packages/shared`, create a new `package.json` file with the command: 

```
$ yarn init -y
```
[code snippet5.javascript](https://gist.github.com/callstack-bot/7324da94e7408a181f49567fc11e7b14#file-code-snippet5-javascript) hosted with ❤ by GitHub

The shared library is in typescript, so we need to set up a build step with the typescript compiler (tsc).

The build step uses `rimraf` which is a small utility for node that allows us to remove a folder, and then build the app with tsc.

`postinstall` indicates that our shared dependency will build automatically after installing.

```
{
  "name": "@example-app/shared",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "rimraf dist && tsc",
    "postinstall": "yarn build",
		"watch": "tsc --watch"
  },
	"dependencies": {
		"react-native": "0.69.1",
		"react": "18.0.0"
	},
	"devDependencies": {
		"rimraf": "^3.0.2",
		"typescript": "^4.4.4"
	},
  "keywords": [],
  "author": "",
  "license": "ISC"
}
```
[code snippet6.json](https://gist.github.com/callstack-bot/e801f1937a8a0fd1bb88d717de1685ff#file-code-snippet6-json) hosted with ❤ by GitHub

Note: The package name is really important, because it will indicate from where we will import our module.

It’s also important to add all packages and keep the **exact same version** used in a shared project and target projects.

Next we need to configure `tsconfig.json` where we can define our build settings.

Here is an example config file:

```
// packages/shared/tsconfig.json
{
    "compilerOptions": {
      "target": "es6",
      "lib": ["dom", "dom.iterable", "esnext"],
      "skipLibCheck": true,
      "esModuleInterop": true,
      "allowSyntheticDefaultImports": true,
      "strict": true,
      "forceConsistentCasingInFileNames": true,
      "module": "commonjs",
      "outDir": "dist", // <- Directory of generated output
      "moduleResolution": "node",
      "resolveJsonModule": true,
      "experimentalDecorators": true,
      "strictPropertyInitialization": false,
      "declaration": true,
      "jsx": "react"
    },
  } 
```
[code snippet7.json](https://gist.github.com/callstack-bot/8d9d72255b264de2fe5982e6875b1ee5#file-code-snippet7-json) hosted with ❤ by GitHub

Next, let’s create an `index.ts` file inside the `packages/shared` folder.

```
export const add = (a: number, b: number) => {
	return a + b;
}
```
[code snippet8.javascript](https://gist.github.com/callstack-bot/623e6fbe1b8241e730478b6c0b24585d#file-code-snippet8-javascript) hosted with ❤ by GitHub

Run this command to build the package:

```
$ yarn build
```
[code snippet9.javascript](https://gist.github.com/callstack-bot/318d2e7c05bbe7576468a600d3a08355#file-code-snippet9-javascript) hosted with ❤ by GitHub

Next, we need to add this package as a dependency in our react native project. So inside `packages/mobile/package.json` add `@example-app/shared` as a dependency.

Note that the version of the package must match the version specified inside packages/shared/package.json. Previously we defined that @example-app/shared version is 1.0.0.

```
// packages/mobile/package.json

"dependencies": {
    "@example-app/shared": "1.0.0",
    ...
  },
```
[code snippet10.json](https://gist.github.com/callstack-bot/854fa05f5f544019f109a0d5b691fd76#file-code-snippet10-json) hosted with ❤ by GitHub

And also inside the metro.config.js

```
// packages/mobile/metro.config.js

module.exports = {
  watchFolders: [
    path.resolve(__dirname, '../../node_modules'),
+   path.resolve(__dirname, '../../node_modules/@example-app/shared'),
  ],
};
```
[code snippet11.javascript](https://gist.github.com/callstack-bot/78f3941f89f05312b9cfcca628b22e86#file-code-snippet11-javascript) hosted with ❤ by GitHub

Now, run this command in the root directory, so yarn will set up symlinks for your shared package.

```
$ yarn
```
[code snippet12.javascript](https://gist.github.com/callstack-bot/f6872dcef8424f3e30326430a86caf8b#file-code-snippet12-javascript) hosted with ❤ by GitHub

Right now we should be able to import the add function inside React Native app.

```
import React from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import {add} from '@example-app/shared';


const App = () => {
  return (
    <View>
      <TouchableOpacity
        accessibilityRole="button"
        onPress={() => {
          console.log(add(1, 2));
        }}>
        <Text>Run Add function</Text>
      </TouchableOpacity>
    </View>
  );
};

export default App;
```
[code snippet13.javascript](https://gist.github.com/callstack-bot/5bc1162031483af83707c89aa8854e74#file-code-snippet13-javascript) hosted with ❤ by GitHub

## Setting up a React project
In order to set up a React project, execute the command below inside `packages/web`:

```
$ npx create-react-app . ExampleApp --template typescript
```
[code snippet14.javascript](https://gist.github.com/callstack-bot/288c2cb2f518d623ee6f41a75fc542a5#file-code-snippet14-javascript) hosted with ❤ by GitHub

Add `@example-app/shared` dependency:

```
// packages/web/package.json
"dependencies": {
    "@example-app/shared": "1.0.0",
    ...
  },
```
[code snippet15.json](https://gist.github.com/callstack-bot/59500de36f12feadf226b26473b2433e#file-code-snippet15-json) hosted with ❤ by GitHub

And we can import and use the shared package:

```
import React from 'react';
import {add} from '@example-app/shared';


const App = () => {
  return (
    <div>
      <button onClick={() => console.log(add(1,2))}>Run add function</button>
    </div>
  );
};

export default App;
```
[code snippet16.javascript](https://gist.github.com/callstack-bot/68fa870f8ea07910622941afe92555a1#file-code-snippet16-javascript) hosted with ❤ by GitHub

## Sharing UI components with React Native Web
In order to share UI components, we need React Native Web which provides a compatibility layer between React DOM and React Native.

## Installation steps
Install react-native-web:

```
$ yarn add react-native-web
```
[code snippet17.txt](https://gist.github.com/callstack-bot/72f809a51365c0b37ded819c5690e1cf#file-code-snippet17-txt) hosted with ❤ by GitHub

Install `babel-plugin-react-native-web`

```
$ yarn add -D babel-plugin-react-native-web
```
[code snippet18.txt](https://gist.github.com/callstack-bot/7cdc04d0036a0a9d4768a7fef7cda4fa#file-code-snippet18-txt) hosted with ❤ by GitHub

## Modify shared package to export React Native component
First inside `packages/shared`, create a new file called `TestComponent.tsx`

```
import { Text, View } from "react-native";
import React from "react";

const TestComponent = () => {
  return (
    <View>
      <Text>TestComponent</Text>
    </View>
  );
};

export default TestComponent;
```
[code snippet 19.javascript](https://gist.github.com/callstack-bot/da59241be3b409632424ee47865fe3d6#file-code-snippet-19-javascript) hosted with ❤ by GitHub

Next export it from the `index.ts file`:

```
export const add = (a: number, b: number) => {
	return a + b;
}

export { default as TestComponent } from './TestComponent.tsx'
```
[code snippet20.javascript](https://gist.github.com/callstack-bot/bb1004a508e5d8ec8f671c27e15a66fe#file-code-snippet20-javascript) hosted with ❤ by GitHub

Rebuild the shared package inside `packages/shared` folder:

```
$ yarn build
```
[code snippet21.txt](https://gist.github.com/callstack-bot/f3f267e8708d9a0ef93a75230228b41a#file-code-snippet21-txt) hosted with ❤ by GitHub

And now we should be able to use `TestComponent` inside a React app.

```
import React from 'react';
import {add, TestComponent} from '@example-app/shared';


const App = () => {
  return (
    <div>
      <button onPress={() => console.log(add(1,2))}>Run add function</button>
			<TestComponent/> // <- React Native component inside React app! 
    </div>
  );
};

export default App;
```
[code snippet22.javascript](https://gist.github.com/callstack-bot/21edfb919df3d7e480d2ff919aa555b7#file-code-snippet22-javascript) hosted with ❤ by GitHub

## To sum up
Setting up a monorepo can be tricky, but it pays off in the long run! You can easily share all of your code and assets between React and React Native.

Using a monorepo is growing in popularity - there is even an open proposal to [migrate react-native repository to monorepo](https://github.com/react-native-community/discussions-and-proposals/pull/480). Find out more about [React Native monorepo](https://github.com/okwasniewski/react-native-monorepo) on github.

_This article was originally published at [callstack.com](https://www.callstack.com/blog/setting-up-react-native-monorepo-with-yarn-workspaces) on October 11, 2022._