React Native + Next.js Monorepo

ecklf

Florentin / 珞辰

Posted on November 14, 2021

React Native + Next.js Monorepo

Preamble

If you need an introduction to Yarn Workspaces: Yarn Blog

If you prefer looking at the finished repository: GitHub

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:

monorepo-tutorial
├── package.json
└── packages
    ├── app
    └── web
Enter fullscreen mode Exit fullscreen mode

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

mkdir monorepo-tutorial && cd monorepo-tutorial && yarn init -y && echo node_modules > .gitignore && git init
Enter fullscreen mode Exit fullscreen mode

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.

yarn add -W react react-dom
Enter fullscreen mode Exit fullscreen mode

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

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

We can now proceed with creating our packages folder.

mkdir packages && cd packages
Enter fullscreen mode Exit fullscreen mode

React Native

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

npx react-native init app --template react-native-template-typescript
Enter fullscreen mode Exit fullscreen mode

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.

cd app
yarn remove react
Enter fullscreen mode Exit fullscreen mode

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.

{
+ "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",
  },
}
Enter fullscreen mode Exit fullscreen mode

React Native Configuration

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

+ module.exports = {
+   reactNativePath: '../../node_modules/react-native',
+ };
Enter fullscreen mode Exit fullscreen mode

Metro Configuration

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

+ const path = require('path');

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

Babel Configuration

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

yarn add -D @babel/runtime babel-plugin-module-resolver
Enter fullscreen mode Exit fullscreen mode
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",
        ],
      },
    ],
  ],
};
Enter fullscreen mode Exit fullscreen mode

iOS / iPadOS

Podfile

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

- 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'
Enter fullscreen mode Exit fullscreen mode

We can confirm if this worked by installing our pods:

npx pod install
Enter fullscreen mode Exit fullscreen mode

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
- 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"
Enter fullscreen mode Exit fullscreen mode

Xcode (workspace) - Bundle React Native code and images

- ../node_modules/react-native/scripts/react-native-xcode.sh
+ ../../../node_modules/react-native/scripts/react-native-xcode.sh
Enter fullscreen mode Exit fullscreen mode

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

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")
}
Enter fullscreen mode Exit fullscreen mode

android/settings.gradle

- 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)
Enter fullscreen mode Exit fullscreen mode

app/build.gradle

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)
Enter fullscreen mode Exit fullscreen mode

Testing the Configuration

yarn start
Enter fullscreen mode Exit fullscreen mode
yarn ios
yarn android
Enter fullscreen mode Exit fullscreen mode

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.

npx create-next-app@latest --ts web
Enter fullscreen mode Exit fullscreen mode
rm package-lock.json && yarn remove react react-dom
Enter fullscreen mode Exit fullscreen mode
{
+ "private": true,
+ "name": "@monorepo/web",
+ "version": "1.0.0",
  "main": "index.js",
  "author": "ecklf",
  "license": "MIT",
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
ecklf
Florentin / 珞辰

Posted on November 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React Native + Next.js Monorepo
react React Native + Next.js Monorepo

November 14, 2021