
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_modulesallprojects {
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_modulesrootProject.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.gradlereact {
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!