Skip to main content
Glama
monorepos.md18 kB
--- title: Work with monorepos description: Learn about setting up Expo projects in a monorepo with Yarn v1 workspaces. --- Monorepos, or _"monolithic repositories"_, are single repositories containing multiple apps or packages. It can help speed up development for larger projects, make it easier to share code, and act as a single source of truth. This guide will set up a simple monorepo with an Expo project. We currently have first-class support for [Yarn 1 (Classic)](https://classic.yarnpkg.com/lang/en/) workspaces. If you want to use another tool, make sure you know how to configure it. > **warning** Monorepos are not for everyone. It requires in-depth knowledge of the used tooling, adds more complexity, and often requires specific tooling configuration. You can get far with just a single repository. ## Automatic configuration Starting from **SDK 48**, Expo automatically detects monorepos and configures new app projects added to a monorepo when using [bun](https://bun.sh/docs/install/workspaces), [npm](https://docs.npmjs.com/cli/using-npm/workspaces), or [yarn](https://yarnpkg.com/features/workspaces). From **SDK 52**, Expo also performs this automatic configuration when using [pnpm](https://pnpm.io/workspaces). The detection is based on the workspaces configuration in your project. > **info** Expo's automatic monorepo setup adds all workspaces in your monorepo as `watchFolders` to Metro. This might slow Metro down for larger monorepos. You can also manually configure which directories Metro should watch by changing the `watchFolders` in your **metro.config.js**, see [Modify the Metro config](#modify-the-metro-config). ## Manual configuration In this example, we will set up a monorepo using Yarn workspaces without the [nohoist](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) option. We will assume some familiar names, but you can fully customize them. After this guide, our basic structure should look like this: - **apps** - Contains multiple projects, including React Native apps. - **packages** - Contains different packages used by our apps. - **package.json** - Root package file, containing Yarn workspaces config. ### Root package file All Yarn monorepos should have a "root" **package.json** file. It is the main configuration for our monorepo and may contain packages installed for all projects in the repository. You can run `yarn init`, or create the **package.json** manually. It should look something like this: ```json package.json { "name": "monorepo", "version": "1.0.0" } ``` ### Set up yarn workspaces Yarn and other tooling have a concept called _"workspaces"_. Every package and app in our repository has its own workspace. Before we can use them, we have to instruct Yarn where to find these workspaces. We can do that by setting the `workspaces` property using [glob patterns](https://classic.yarnpkg.com/lang/en/docs/workspaces/#toc-tips-tricks), in the **package.json**: ```json package.json { "private": true, "name": "monorepo", "version": "1.0.0", "workspaces": ["apps/*", "packages/*"] } ``` > **warning** Yarn workspaces require the root **package.json** to be private. If you don't set this, `yarn install` will error with a message mentioning this. ### Create our first app Now that we have the basic monorepo structure set up, let's add our first app. Before we create our app, we have to create the **apps** directory. This directory contains all separate apps or websites that belong to this monorepo. Inside this **apps** directory, we can create a sub-directory that contains the React Native app. > If you have an existing app, you can copy all those files into a directory inside **apps**. After copying or creating the first app, run `yarn` to check for common warnings. #### Modify the Metro config Expo's Metro config has monorepo support for Bun, npm, pnpm and Yarn. You don't have to manually configure Metro when using monorepos if you use the config from [`expo/metro-config`](/guides/customizing-metro/). If that's the case, you can skip this step. To configure a monorepo with Metro manually, there are two main changes: 1. Make sure Metro is watching all relevant code within the monorepo, not just **apps/cool-app**. 2. Tell Metro where it can resolve packages. They might be installed in **apps/cool-app/node_modules** or **node_modules**. We can configure this by [creating a **metro.config.js**](/guides/customizing-metro/#customizing) with the following content: ```js metro.config.js const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); // Find the project and workspace directories const projectRoot = __dirname; // This can be replaced with `find-yarn-workspace-root` const monorepoRoot = path.resolve(projectRoot, '../..'); const config = getDefaultConfig(projectRoot); // 1. Watch all files within the monorepo config.watchFolders = [monorepoRoot]; // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; module.exports = config; ``` > Learn more about [customizing Metro](/guides/customizing-metro). Metro has three separate stages in its bundling process, [documented here](https://metrobundler.dev/docs/concepts). During the first phase, **Resolution**, Metro resolves your app's required files and dependencies. Metro does that with the `watchFolders` option, which is set to the project directory by default. This default setting works great for apps that don't use a monorepo structure. When using monorepos, your app dependencies splits up into different directories. Each of these directories must be within the scope of the [`watchFolders`](https://metrobundler.dev/docs/configuration/#watchfolders). If a changed file is outside of that scope, Metro won't be able to find it. Setting this path to the root of your monorepo will force Metro to watch all files within the repository and possibly cause a slow initial startup time. As your monorepo increases in size, watching all files within the monorepo becomes slower. You can speed things up by only watching the packages your app uses. Typically, these are the ones that are installed with an asterisk (\*) in your **package.json**. For example: ```js metro.config.js const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); const projectRoot = __dirname; const monorepoRoot = path.resolve(projectRoot, '../..'); const config = getDefaultConfig(projectRoot); // Only list the packages within your monorepo that your app uses. No need to add anything else. // If your monorepo tooling can give you the list of monorepo workspaces linked // in your app workspace, you can automate this list instead of hardcoding them. const monorepoPackages = { '@acme/api': path.resolve(monorepoRoot, 'packages/api'), '@acme/components': path.resolve(monorepoRoot, 'packages/components'), }; // 1. Watch the local app directory, and only the shared packages (limiting the scope and speeding it up) // Note how we change this from `monorepoRoot` to `projectRoot`. This is part of the optimization! config.watchFolders = [projectRoot, ...Object.values(monorepoPackages)]; // Add the monorepo workspaces as `extraNodeModules` to Metro. // If your monorepo tooling creates workspace symlinks in the `node_modules` directory, // you can either add symlink support to Metro or set the `extraNodeModules` to avoid the symlinks. // See: https://metrobundler.dev/docs/configuration/#extranodemodules config.resolver.extraNodeModules = monorepoPackages; // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; module.exports = config; ``` This option is important to resolve libraries in the correct **node_modules** directories. Monorepo tooling, like Yarn, usually creates two different **node_modules** directories which are used for a single workspace. 1. **apps/mobile/node_modules** - The "project" directory 2. **node_modules** - The "root" directory Yarn uses the root directory to install packages used in multiple workspaces. If a workspace uses a different package version, it installs that different version in the project directory. We have to tell Metro to look in these two directories. The order is important here because the project directory **node_modules** can contain specific versions we use for our app. When the package does not exist in the project directory, it should try the shared root directory. #### Change default entrypoint In monorepos, we can't hardcode paths to packages anymore since we can't be sure if they are installed in the root **node_modules** or the workspace **node_modules** directory. If you are using a managed project, we have to change our default entry point that looks like `node_modules/expo/AppEntry.js`. > This new entry point already exists for existing React Native (bare) projects. You only need to add this if you have a project generated with `npx create-expo-app`. If you are using [Expo Router](/router/introduction/), do not change the entrypoint. By default [`EXPO_USE_METRO_WORKSPACE_ROOT`](/more/expo-cli/#environment-variables) is activated, which enables the auto server root detection for Metro. You can skip this step. Otherwise, open your app's **package.json**, change the `main` property to `index.js`, and create this new **index.js** file in the app directory with the content below. ```js index.js // registerRootComponent calls AppRegistry.registerComponent('main', () => App); // It also ensures that whether you load the app in Expo Go or in a native build, // the environment is set up appropriately registerRootComponent(App); ``` ### Create a package Monorepos can help us group code in a single repository. That includes apps but also separate packages. They also don't need to be published. The [Expo repository](https://github.com/expo/expo) uses this as well. All the Expo SDK packages live inside the [**packages**](https://github.com/expo/expo/tree/main/packages) directory in our repo. It helps us test the code inside one of our [**apps**](https://github.com/expo/expo/tree/main/apps/native-component-list) directory before we publish them. Let's go back to the root and create the **packages** directory. This directory can contain all the separate packages that you want to make. Once you are inside this directory, we need to add a new sub-directory. The sub-directory is a separate package that we can use inside our app. In the example below, we named it **cool-package**. We won't go into too much detail in creating a package. If you are not familiar with this, consider using a simple app without monorepos. But, to make the example complete, let's add an **index.js** file with the following content: ```js index.js ``` ### Using the package Like standard packages, we need to add our **cool-package** as a dependency to our **cool-app**. The main difference between a standard package, and one from the monorepo, is you'll always want to use the _"current state of the package"_ instead of a version. Let's add **cool-package** to our app by adding `"cool-package": "*"` to our app **package.json** file: ```json package.json { "name": "cool-app", "version": "1.0.0", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web" }, "dependencies": { "cool-package": "*", "expo": "~53.0.0", "expo-status-bar": "~2.2.3", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.4", "react-native-web": "~0.20.0" }, "devDependencies": { "@babel/core": "^7.27.7" } } ``` > After adding the package as a dependency, run `yarn install` to install or link the dependency to your app. Now you should be able to use the package inside your app! To test this, let's edit the **App.js** in our app and render the `greeting` text from our **cool-package**. ```jsx App.js return ( <Text>{greeting}</Text> ); } ``` ## Common issues As mentioned earlier, using monorepos is not for everyone. You take on increased complexity and need to solve issues you most likely will run into. Here are a couple of common issues you might encounter. ### Multiple React Native versions within the monorepo Expo SDK has improved support for more complete **node_modules** patterns, such as isolated modules. Unfortunately, React Native can still cause issues when installing multiple versions inside a single monorepo. Because of that, it's recommended to only use a single version of React Native. You can check if your monorepo has multiple React Native versions and why they are installed through the package manager you use. ### Can I use another monorepo tool instead of Yarn workspaces? There are a lot of monorepo tools available, and each of these tools has its benefits. It's hard for us to keep up with the latest tools and methods, and because of that, we can't officially support new monorepo tools. That being said, if the tool follows these three rules, it should work fine. React Native dependencies contain many other files besides JavaScript, like Gradle files such as **react-native/react.gradle**. These native files are referenced from different sources other than Node.js, and because of that, it makes it fundamentally incompatible with concepts like Plug'n'Play modules. Whenever multiple workspaces use the same version of a single dependency, they can be installed in a root **node_modules** directory. Monorepo tools usually do this to remove duplicate tasks, like installing the same dependency twice in different places. This rule isn't necessary but does set us up for next rule. In the [Modify the Metro config](#modify-the-metro-config) step, we instructed Metro to do a couple of this, specifically: - #2 - Resolve dependencies in the order of the local **apps/&lt;name&gt;/node_modules** and root **node_modules** directories. - #3 - Disable resolving dependencies using the hierarchical lookup strategy. If a workspace uses a different library version than the one installed in the root **node_modules**, that different library version must be installed in the workspace **apps/&lt;name&gt;/node_modules** directory. When Metro resolves a library, for example, `react`, from the workspace, it should find that different version in **apps/&lt;name&gt;/node_modules** and not look inside the root **node_modules** directory. When importing a dependency from the root **node_modules** directory that also imports `react`, `react` should still resolve to the different version installed in **apps/&lt;name&gt;/node_modules**. That's what the disabled hierarchical lookup option does for Metro. Without this, some libraries might import the wrong `react` version and cause "multiple React versions found" errors. The default settings of tools like [pnpm](https://pnpm.io/) do not follow these rules. You can change that by adding a **.npmrc** file with `node-linker=hoisted` ([see docs](https://pnpm.io/npmrc#node-linker)). That config option will change the behavior to match these rules. ### Script '...' does not exist React Native uses packages to ship both JavaScript and native files. These native files also need to be linked, like the [**react-native/react.Gradle**](https://github.com/facebook/react-native/blob/v0.70.6/react.gradle) file from **android/app/build.Gradle**. Usually, this path is hardcoded to something like: **Android** ([source](https://github.com/facebook/react-native/blob/e918362be3cb03ae9dee3b8d50a240c599f6723f/template/android/app/build.gradle#L84)) ```groovy apply from: "../../node_modules/react-native/react.gradle" ``` **iOS** ([source](https://github.com/facebook/react-native/blob/e918362be3cb03ae9dee3b8d50a240c599f6723f/template/ios/Podfile#L1)) ```ruby require_relative '../node_modules/react-native/scripts/react_native_pods' ``` Unfortunately, this path can be different in monorepos because of [hoisting](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/). It also doesn't use the [Node module resolution](https://nodejs.org/api/modules.html#all-together). You can avoid this issue by using Node to find the location of the package instead of hardcoding this: **Android** ([source](https://github.com/expo/expo/blob/6877c1f5cdca62b395b0d5f49d87f2f3dbb50bec/templates/expo-template-bare-minimum/android/app/build.gradle#L87)) ```groovy apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.gradle") ``` **iOS** ([source](https://github.com/expo/expo/blob/61cbd9a5092af319b44c319f7d51e4093210e81b/templates/expo-template-bare-minimum/ios/Podfile#L2)) ```ruby require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") ``` In the snippets above, you can see that we use Node's own [`require.resolve()`](https://nodejs.org/api/modules.html#requireresolverequest-options) method to find the package location. We explicitly refer to `package.json` because we want to find the root location of the package, not the location of the entry point. And with that root location, we can resolve to the expected relative path within the package. [Learn more about these references here](https://github.com/expo/expo/blob/4633ab2364e30ea87ca2da968f3adaf5cdde9d8b/packages/expo-modules-core/README.md#importing-native-dependencies---autolinking). All Expo SDK modules and templates have these dynamic references and work with monorepos. However, occasionally, you might run into packages that still use the hardcoded path. You can manually edit it with [`patch-package`](https://github.com/ds300/patch-package#readme) or mention this to the package maintainers.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jaksm/expo-docs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server