Disclaimer: This article is revised version of this great Callstack article. It seemed some things were outdated so I wrote this to share how I got a 0.78 React Native project working with yarn workspaces in 2025. Enjoy!

What is a monorepo?

A monorepo is a single repository that holds a multitude of projects with all their code and assets. While the projects can be related, they can be used independently by different teams.

Working with monorepos is very useful, especially when developing big and complex applications like super apps. Monorepos enable sharing the logic between a web app and a mobile app, for example.

In this article, we’re going to set up a basic structure for a monorepo in React Native.

Why use monorepo in React Native?

Thanks to react-native-web we can share every component so there’s no need to duplicate the code many times. This means easier dependency management, shorter CI times, and better collaboration between teams since the code can be shared.

The main advantage though is the possibility of sharing packages between React and React Native. Most of the time we decide to share only business logic, but thanks to React Native Web, we can also share some of the UI components.

Setting up Yarn workspaces

While setting up a monorepo, we have two options: we can either hoist the packages to the root level or prevent them from hoisting. Yarn workspaces have an option named nohoist which allows us to specify packages that aren’t hoisted. It depends on your use case but most of the time it’s better to hoist packages to the root level.

Alternatively, you can use npm workspaces, it should work the same.

Why it’s better to avoid using nohoist

Preventing packages from hoisting takes back the main advantage of monorepos which is sharing node_modules between repositories. When nohoist option is turned on, most of the time packages are duplicated inside the root level node_modules and inside the project level node_modules. Downloading packages multiple times can lead to longer CI/CD runs and higher costs.

Rearranging the project structure

In order to set up yarn workspaces, we need to restructure our project to suit the structure below. Projects need to be divided into separate folders and we should have root level package.json

your-project
    packages
        web // <- React app
        mobile // <- React Native app
        shared // <- Shared (includes files shared between mobile/web)

Next, you should create a package.json file in the root directory of our project with this command: $ yarn init -y

It should look like this:

{
  "private": "true",
  "name": "example-app",
  "version": "1.0.0",
  "workspaces": [
    "packages/*"
  ]
}

Note that private: true is required because workspaces are not meant to be published.

In the workspaces section, we define a work tree. We can pass there an array of glob patterns that should be used to locate the workspaces. So in the example above, every folder inside packages is defined as a workspace.

Creating a React Native project

Use command below to create a React Native project inside the packages folder:

npx @react-native-community/cli@latest init AwesomeProject

In order for our app to work in a monorepo structure, we need to make sure that config files are pointing to the root level node_modules due to package hoisting.

iOS setup

Regenerate Pods, by entering the command below in the packages/mobile folder:

cd ios && pod install

Android setup

Note: At the time of creating this project, 0.78.1 was the latest version of RN. The Callstack article has steps for older versions.
In top-level build.gradle add this:

allprojects {
    project.pluginManager.withPlugin("com.facebook.react") {
        react {
            reactNativeDir = rootProject.file("../../../node_modules/react-native/")
            codegenDir = rootProject.file("../../../node_modules/@react-native/codegen/")
        }
    }
}

and inside: packages/mobile/android/settings.gradle

- pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
+ pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") }
- includeBuild('../node_modules/react-native-gradle-plugin')
+ includeBuild('../../../node_modules/react-native-gradle-plugin')

Configure Metro

We’re almost done with setting up the project. The last thing in the React Native app is to add watchFolders so metro knows where the linked node_modules are. The shared modules are symlinked by Yarn, and Metro started supporting symlinks from version 0.75.1, effectively being shipped with React Native 0.72.

Regardless of whether you are using symlinks, metro still requires us to list symlinked modules in the config, but this might change in the future. You can follow the discussion on GitHub.

const path = require("path");

// packages/mobile/metro.config.js
module.exports = {
  // ...
  watchFolders: [
    path.resolve(__dirname, '../../node_modules'),
    path.resolve(__dirname, '../../node_modules/@example-app/shared'),
  ],
};

Ready!

Install your dependencies with yarn in your monorepo's root directory. You should see all your dependencies in the root node_modules folder like so:
dependencies in root node_modules folder

And the mobile/node_modules folder should only contain a .bin folder:

mobile/.bin folder

I recommend adding a clean script to your root package.json to help clear stubborn modules:

"scripts": {
    "clean": "find . -type d \\( -name \"node_modules\" -o -name \".kotlin\" -o -name \".gradle\" -o -name \"build\" -o -name \"Pods\" \\) -prune -exec rm -rf {} +"
  }

Then start your server with:

yarn workspace mobile start

And build your iOS or Android with their respective scripts:

yarn workspace mobile ios

or

yarn workspace mobile android

To sum up

Setting up a monorepo can be tricky, but it pays off in the long run! You can easily share all of your code and assets between React and React Native.

Using a monorepo is growing in popularity - there is even an open proposal to migrate react-native repository to monorepo. Find out more about React Native monorepo on github.

What changed from 2022

  1. Creating React Native Project:
  2. iOS Setup:

    • Did not edit paths in Podfile since there were no require_relative statements.
    • Did not edit WITH_ENVIRONMENT and REACT_NATIVE_XCODE paths since they already responded to the React Native path:

      set -e
      
      WITH_ENVIRONMENT="$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
      REACT_NATIVE_XCODE="$REACT_NATIVE_PATH/scripts/react-native-xcode.sh"
      
  3. Android Setup:

    • Did not uncomment the line in packages/android/app/build.gradle
    • Updated codegenDir path. "../../../node_modules/react-native-codegen/"module moved to "../../../node_modules/@react-native/codegen/"
    • Didn't add the apply from line to settings.gradle since it was causing the build process to hang.
    • Updated includeBuild path in two places. (not sure if necessary).
    • Didn't include the steps for < RN 0.71 since I didn't try them.
  4. Metro Configuration:

    • Didn't include unstable_enableSymlinks. Might be deprecated since it didn't have an effect.