React Native + Next.js Monorepo

Tags: react, reactnative, nextjs, yarnworkspaces
Published: 2021-11-14 11:23:22

## Preamble

If you need an introduction to Yarn Workspaces: [Yarn Blog](https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/)

If you prefer looking at the finished repository: [GitHub](https://github.com/ecklf/react-native-next-monorepo.git)

## Initial Setup

Our goal for this blog post is to have a basic monorepo setup that contains one bare React Native app and one Next.js project. This will result in a file structure like this:

```shell
monorepo-tutorial
├── package.json
└── packages
    ├── app
    └── web
```

For starters we create our root directory and initialize a fresh project with git repository.

```shell
mkdir monorepo-tutorial && cd monorepo-tutorial && yarn init -y && echo node_modules > .gitignore && git init
```

Since both of our packages will depend on `react` we will _lift up_ the dependency to the root level of our monorepo. Note that we also add `react-dom` in case we want to create more web packages later.

```shell
yarn add -W react react-dom
```

In our `package.json` we define a workspace structure. The below _glob_ defined in `workspaces` tells Yarn where our monorepo packages are located.

```diff
{
+ "private": true,
+ "name": "root",
  "version": "1.0.0",
  "main": "index.js",
  "author": "ecklf",
  "license": "MIT",
+ "workspaces": [
+   "packages/*"
+ ]
}
```

We can now proceed with creating our packages folder.

```shell
mkdir packages && cd packages
```

## React Native

Let's start by initializing a fresh React Native project from the template:

```shell
npx react-native init app --template react-native-template-typescript
```

You should now encouter this error:

> Failed to install CocoaPods dependencies for iOS project.

This is perfectly fine since the template's CocoaPods configuration has the wrong path to `react-native`.

Continue by removing the `react` dependency from the template since we will resolve it from the root level.

```shell
cd app
yarn remove react
```

From my experience Metro plays the nicest in monorepos when launched separately with `yarn start`, so we disable the packaging when running `ios` / `android` scripts. While we are at it we can also update the name in our `package.json`.

```diff
{
+ "private": true,
+ "name": "@monorepo/app",
  "version": "1.0.0",
  "main": "index.js",
  "author": "ecklf",
  "license": "MIT",
  "scripts": {
-   "android": "react-native run-android",
+   "android": "react-native run-android --no-packager",
-   "ios": "react-native run-ios",
+   "ios": "react-native run-ios --no-packager",
  },
}
```

### React Native Configuration

Create the file `react-native.config.js` with the following content:

```diff
+ module.exports = {
+   reactNativePath: '../../node_modules/react-native',
+ };
```

### Metro Configuration

Update `metro.config.js` to have an additional watch folder at root level.

```diff
+ const path = require('path');

module.exports = {
+ watchFolders: [path.resolve(__dirname, '../../')],
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};
```

### Babel Configuration

We need to add aliases to explicitly define where our root-level packages are located in `babel.config.js`.

```shell
yarn add -D @babel/runtime babel-plugin-module-resolver
```

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

module.exports = {
  presets: ["module:metro-react-native-babel-preset"],
  plugins: [
    [
      "module-resolver",
      {
        root: ["./src"],
        alias: {
          react: require.resolve("react", {
            paths: [path.join(__dirname, "./")],
          }),
          "^react-native$": require.resolve("react-native", {
            paths: [path.join(__dirname, "./")],
          }),
          "^react-native/(.+)": ([, name]) =>
            require.resolve(`react-native/${name}`, {
              paths: [path.join(__dirname, "./")],
            }),
        },
        extensions: [
          ".ios.js",
          ".ios.ts",
          ".ios.tsx",
          ".android.js",
          ".android.ts",
          ".android.tsx",
          ".native.js",
          ".native.ts",
          ".native.tsx",
          ".js",
          ".ts",
          ".tsx",
        ],
      },
    ],
  ],
};
```

### iOS / iPadOS

#### Podfile

First, we fix our previous install error by now pointing to our root's `node_modules` folder.

```diff
- require_relative '../node_modules/react-native/scripts/react_native_pods'
+ 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-community/cli-platform-ios/native_modules'
```

We can confirm if this worked by installing our pods:

```shell
npx pod install
```

#### Xcode (workspace) - Signing & Capabilities

Add your development team to build the project.

#### Xcode (workspace) - Build Phases

Nothing special here. We just adjust the paths like in CocoaPods.

##### Start Packager

```diff
- echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../node_modules/react-native/scripts/.packager.env"
+ echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../../../node_modules/react-native/scripts/.packager.env"

- open "$SRCROOT/../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
+ open "$SRCROOT/../../../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
```

#### Xcode (workspace) - Bundle React Native code and images

```diff
- ../node_modules/react-native/scripts/react-native-xcode.sh
+ ../../../node_modules/react-native/scripts/react-native-xcode.sh
```

#### Build Settings

##### User-Defined

Add a user-defined setting (+ sign at the top menu bar) `RCT_NO_LAUNCH_PACKAGER` with the value `1`.

### Android

Getting things to work on Android is just a matter of adding paths for `hermes` + `react-native` cli and updating the existing ones.

#### android/build.gradle

```diff
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")
}
```

#### android/settings.gradle

```diff
- apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
+ apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
```

#### app/build.gradle

```diff
project.ext.react = [
-  enableHermes: false,  // clean and rebuild if changing
+  enableHermes: true,  // clean and rebuild if changing
+  hermesCommand: "../../../../node_modules/hermes-engine/%OS-BIN%/hermesc",
+  composeSourceMapsPath: "../../node_modules/react-native/scripts/compose-source-maps.js",
+  cliPath: "../../node_modules/react-native/cli.js"
]

- apply from: "../../node_modules/react-native/react.gradle"
+ apply from: "../../node_modules/react-native/react.gradle"

- def hermesPath = "../../node_modules/hermes-engine/android/";
+ def hermesPath = "../../../../node_modules/hermes-engine/android/";

- 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)
```

### Testing the Configuration

```shell
yarn start
```

```shell
yarn ios
yarn android
```

## Next.js

Fortunately adding a Next.js project is more straightforward. All we need to do is delete `package-lock.json` (we use yarn not npm) and remove our root dependencies from the template.

```shell
npx create-next-app@latest --ts web
```

```shell
rm package-lock.json && yarn remove react react-dom
```

```diff
{
+ "private": true,
+ "name": "@monorepo/web",
+ "version": "1.0.0",
  "main": "index.js",
  "author": "ecklf",
  "license": "MIT",
}
```