React Native + Next.js Monorepo
Florentin / 珞辰
Posted on November 14, 2021
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
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
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
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/*"
+ ]
}
We can now proceed with creating our packages folder.
mkdir packages && cd packages
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
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
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",
},
}
React Native Configuration
Create the file react-native.config.js
with the following content:
+ module.exports = {
+ reactNativePath: '../../node_modules/react-native',
+ };
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,
},
}),
},
};
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
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.
- 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:
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
- 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
- ../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
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
- 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
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
yarn start
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.
npx create-next-app@latest --ts web
rm package-lock.json && yarn remove react react-dom
{
+ "private": true,
+ "name": "@monorepo/web",
+ "version": "1.0.0",
"main": "index.js",
"author": "ecklf",
"license": "MIT",
}
Posted on November 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.