Log in to GraphQL EditorGet started
React Native monorepo with shared components & TypeScript
Marcin

Marcin Falkowski

2/28/2023

React Native monorepo with shared components & TypeScript

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.

Setting up workspaces with Yarn

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.

Project structure

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.

Creating a package.json file in the root directory

  1. open your terminal and go to the root directory cd my-app,
  2. create a package.json file with yarn init -y,
  3. open and check your package.json file. It should look something like this:
{
  "private": "true",
  "name": "my-app",
  "version": "1.0.0",
  "workspaces": ["packages/*"]
}
  • Private: true is required. That's very important because we don't want to make our workspaces public.
  • Name: the name of your monorepo.
  • Workspaces: our workspace with the projects structured in a tree. For now, it's okay as is.

Creating React Native projects

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:

IOS configuration

  1. Open 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'
  1. Open your terminal and install pods. If you are in the packages/app* folder run:
	cd ios
	pod install
  1. Open XCode and your project, select the Project navigator and click on the name of your project. This will open project settings.

XCode

  1. Click 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"

XCode

!Remember to repeat this for each app in your monorepo!

Android setup

For React Native <= 0.71

  1. Open 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' }
    }
}
  1. Open 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')
}
  1. Open myapp/packages/app1/android/app/build.gradle and override the default location of the cli
project.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

  1. Go to myapp/packages/app1/android/app/build.gradle
  2. Find and edit the following:
react {
   hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
}
  1. Go to myapp/packages/app1/android/build.gradle and add
allprojects {
    project.pluginManager.withPlugin("com.facebook.react") {
        react {
            reactNativeDir = rootProject.file("../../../node_modules/react-native/")
            codegenDir = rootProject.file("../../../node_modules/react-native-codegen/")
        }
    }
}
  1. Go to the myapp/packages/app1/andoird/settings.gradle and add
apply 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!

Metro configuration

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')],
};

Shared package

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!

Check out our other blogposts

Making a simple Eleventy blog template
Michał Tyszkiewicz
Michał Tyszkiewicz
Making a simple Eleventy blog template
5 min read
about 1 year ago
Top tools for a junior frontend developer
Robert Matyszewski
Robert Matyszewski
Top tools for a junior frontend developer
5 min read
over 5 years ago
GraphQL tools & libraries
Michał Tyszkiewicz
Michał Tyszkiewicz
GraphQL tools & libraries
4 min read
about 4 years ago

Ready for take-off?

Elevate your work with our editor that combines world-class visual graph, documentation and API console

Get Started with GraphQL Editor