Marcin Falkowski
2/28/2023
To start we should ask a simple question: why should I use a monorepo for React Native apps? It's actually very useful when you have two (or more) similar applications. Monorepos enable sharing of components, functions and logic between all of the included applications. This can unlock a variety of benefits like core sharing, simplified ci/cd process and consistent tooling.
When creating a monorepo for react native applications, it is better to avoid using nohoist because we don't need duplicates inside the root level and inside the project level node_modules
.
Our projects need to be divided into subfolders. We treat them as separate entities. Here’s an example of how our project structure can look like:
my-app/
packages/
app1/ <- first app
app2/ <- second app
shared/ <- folder for shared components, logic, etc.
cd my-app
,yarn init -y
,{
"private": "true",
"name": "my-app",
"version": "1.0.0",
"workspaces": ["packages/*"]
}
To create React Native projects we can use the following commands:
cd packages
npx react-native init FirstApp --directory app1 --template react-native-template-typescript
npx react-native init SecondApp --directory app2 --template react-native-template-typescript
If your app names and directories match you can use this instead:
cd packages
npx react-native init app1 --template react-native-template-typescript
npx react-native init app2 --template react-native-template-typescript
where app1
and app2
are the names of both our apps and their directories.
Most likely your apps have now been created but pods will not install. No worries, we’ll fix this in the next step:
Podfile
go into your app and change paths to the node_modules
- 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'
cd ios
pod install
Build Phases
, open Bundle React Native code and images
and change the script paths:- 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"
!Remember to repeat this for each app in your monorepo!
For React Native <= 0.71
myapp/packages/app1/android/build.gradle
and change the paths to node_modules
allprojects {
repositories {
maven {
// All 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")
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions there.
content {
excludeGroup "com.facebook.react"
}
}
google()
maven { url 'https://www.jitpack.io' }
}
}
myapp/packages/app1/android/settings.gradle
and change the paths to node_modules
rootProject.name = 'FirstApp'
- apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
+ apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle");
applyNativeModulesSettingsGradle(settings)
include ':app'
- includeBuild('../node_modules/react-native-gradle-plugin')
+ includeBuild('../../../node_modules/react-native-gradle-plugin')
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')
}
myapp/packages/app1/android/app/build.gradle
and override the default location of the cliproject.ext.react = [
enableHermes: true, // clean and rebuild if changing
+ cliPath: "../../../../node_modules/react-native/cli.js",
]
In the same file change these lines:
- apply from: "../../node_modules/react-native/react.gradle"
+ apply from: "../../../../node_modules/react-native/react.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)
if (isNewArchitectureEnabled()) {
// We configure the CMake build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
cmake {
arguments "-DPROJECT_BUILD_DIR=$buildDir",
- "-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
+ "-DREACT_ANDROID_DIR=$rootDir/../../node_modules/react-native/ReactAndroid",
- "-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
+ "-DREACT_ANDROID_BUILD_DIR=$rootDir/../../node_modules/react-native/ReactAndroid/build",
- "-DNODE_MODULES_DIR=$rootDir/../node_modules",
+ "-DNODE_MODULES_DIR=$rootDir/../../node_modules",
"-DANDROID_STL=c++_shared"
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
ndk {
abiFilters (*reactNativeArchitectures())
}
}
}
For React Native >= 0.71
myapp/packages/app1/android/app/build.gradle
react {
hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
}
myapp/packages/app1/android/build.gradle
and addallprojects {
project.pluginManager.withPlugin("com.facebook.react") {
react {
reactNativeDir = rootProject.file("../../../node_modules/react-native/")
codegenDir = rootProject.file("../../../node_modules/react-native-codegen/")
}
}
}
myapp/packages/app1/andoird/settings.gradle
and addapply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
includeBuild('../../../node_modules/react-native-gradle-plugin')
!Remember to repeat this for every app in your monorepo!
Now we should indicate for metro where node_modules
are located. Go to the metro.config.js
file and add watchFolders
:
const path = require('path');
module.exports = {
watchFolders: [path.resolve(__dirname, '../../node_modules')],
};
Inside the shared folder, create a package.json
file.
cd shared
yarn init -y
Your package.json
file should look something like this:
{
"name": "@my-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": ""
}
Close your package.json
and now let's create a tsconfig.json
file:
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"declaration": true,
"jsx": "react",
"baseUrl": "./src"
}
}
Now go into your shared folder, and create an src folder there. Its structure should look something like mine:
src/
index.ts
compontents/
index.ts
atoms/
index.ts
Test.tsx
Where Test.tsx
is a React Function component :)
Now, we can export this in index.ts
in a shared
folder via:
export * from './src'
Now let's create a build of our shared
directory:
yarn build
With that, we have finished setting up our shared
folder. Now we should indicate this directory to our applications. So now we need to add our shared
dependency to the package.json
file in all our applications:
"dependencies" : {
"@my-app/shared": "^1.0.0"
}
and for metro.config.js
:
module.exports = {
watchFolders: [
path.resolve(__dirname, '../../node_modules'),
path.resolve(__dirname, '../../node_modules/@my-app/shared'),
],
};
Then go back into your root directory and run:
yarn
Now we should be able to import things from shared
directly into our React Native Apps:
import React, { type PropsWithChildren } from 'react';
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import { Test } from '@my-app/shared';
const Section: React.FC<
PropsWithChildren<{
title: string;
}>
> = ({ children, title }) => {
const isDarkMode = useColorScheme() === 'light';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
};
const App = () => {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: !isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={!isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<Header />
<Test />
<View
style={{
backgroundColor: !isDarkMode ? Colors.black : Colors.white,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
});
export default App;
And that’s how it works! Enjoy!