---
title: "A Practical Guide to React Native Monorepo With Yarn Workspaces"
description: "Learn how to set up a React Native monorepo with Yarn workspaces for mobile and web, including Metro, Gradle, and shared packages."
url: https://www.callstack.com/blog/a-practical-guide-to-react-native-monorepo-with-yarn-workspaces
updated: 2026-10-24T08:39:26.320Z
---

# A Practical Guide to React Native Monorepo With Yarn Workspaces

Learn how to set up a React Native monorepo with Yarn workspaces for mobile and web, including Metro, Gradle, and shared packages.

_By Nazar Sydiaha, Software Engineer @ Callstack · Published 2026-02-18 · Category: React Native_

In this article, we’ll walk through setting up a monorepo for a **mobile** plus **web** React Native project.

**Note:** This guide covers a monorepo setup with [React Native Community CLI](https://github.com/react-native-community/cli). For Expo-based projects, see the official [Expo monorepo documentation](https://docs.expo.dev/guides/monorepos/).

## What is a monorepo?

A monorepo is a single repository that contains multiple projects along with all their code and assets. While those projects are often related, they can still be developed, tested, and released independently by different teams.

Monorepos are especially useful when working on large and complex products, such as [super apps](https://www.callstack.com/blog/why-have-a-super-app). They make it easier to share code between platforms, for example, between a mobile application and a web application, without duplicating logic or reinventing the same solutions in multiple places.

## Why use a monorepo in React Native?

As React Native applications grow, it’s common to split code into multiple apps and shared modules:  for example, a mobile app, a web app, and shared packages containing UI components, hooks, or business logic. Managing these pieces across separate repositories quickly becomes painful: duplicated code, mismatched dependency versions, and fragmented tooling.

A monorepo solves this by keeping all applications and shared packages in a single repository. This makes dependency management predictable, reduces CI overhead, and allows teams to collaborate on shared code without publishing intermediate packages or syncing changes across repos.

React Native additionally supports [platform-specific file extensions](https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions), such as `.web.tsx` and `.native.tsx`. This allows us to share package APIs while providing platform-optimized implementations where needed without extra runtime layers. As a result, we can reuse logic and UI contracts across mobile and web while keeping each platform clean and efficient.

## Workspace strategy

### Rearranging the project structure

To set up Yarn workspaces, we first need to reorganize the project structure. A monorepo requires a clear separation between applications and shared code, with a single package.json at the repository root managing dependencies and workspace configuration.

In this article, we’ll use a structure that has proven to scale well for multi-platform products. It’s also one of the recommended approaches for building universal applications, as described in [The Ultimate Guide to React Native TV Development](https://www.callstack.com/ebooks/the-ultimate-guide-to-react-native-tv-development).

```javascript
your-project/
	├── apps/
	│   ├── mobile/        # React Native app
	│   └── web/           # Next.js web app
	│
	├── packages/
	│   ├── api/            # API clients, SDKs
	│   ├── config/         # Shared config/env
	│   ├── hooks/          # Shared React hooks
	│   ├── ui/             # Cross-platform UI components
	│   └── utils/          # Pure JS helpers
```

### Why separate apps and packages?

This separation helps maintain clear ownership boundaries. Applications depend on packages, but packages never depend on applications. This makes the dependency graph predictable and allows the monorepo to scale as more apps or shared modules are added.

## Setting up Yarn workspaces

### Preparing the repository

To turn the repository into a Yarn-driven monorepo, we need to run a small sequence of commands at the root level:

```bash
corepack enable
```

Enables [Corepack](https://github.com/nodejs/corepack) so the project can control which Yarn version is used, regardless of what’s installed globally.

```bash
yarn set version stable
```

Installs and pins a stable Yarn version directly in the repository to ensure the same behavior across machines.`‍`

```bash
yarn config set nodeLinker node-modules
```

By default, modern versions of Yarn use [Plug’n’Play (PnP)](https://yarnpkg.com/features/pnp). While PnP works well for many projects, React Native tooling is still heavily optimized for the classic `node_modules` layout, so we go with the regular `node-modules` linker.

```bash
yarn init -w -p
```

This initializes the project as a Yarn workspace:

- `w` (`--workspace`) marks the root as a workspace root.
- `p` (`--private`) sets `"private": true` in `package.json`, which is required because workspace roots are not meant to be published.

At this point, you’ll have a root-level `package.json` that will manage dependencies for the entire monorepo.

### Configuring workspaces

Now open the generated `package.json` and add `“apps”` to the workspaces array. The final version should look like the following:

```json
{
  "name": "example-monorepo",
  "packageManager": "yarn@4.12.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
}
```

The `workspaces` section defines a work tree. We can pass 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.

### Defining dependency resolution

In a monorepo, it’s important that core dependencies like `react` resolve to a single version across all workspaces. Even small mismatches (such as different patch versions of React) can lead to runtime errors and unexpected crashes.

In the same `package.json` we can explicitly define how these dependencies are resolved at the workspace root. Add a `resolutions` section:

```json
{
  "resolutions": {
    "react": "19.2.3",
    "react-dom": "19.2.0",
  }
}
```

These versions reflect the defaults generated by Next.js and React Native at the time of writing, so adjust them to match your project.

## Creating the React Native application

With the workspace configured, we can now create the React Native app inside the `apps/mobile`.

### Initializing React Native inside a workspace

From the repository root, run:

```bash
npx @react-native-community/cli init mobile \
  --directory apps/mobile \
  --skip-git-init \
  --skip-install
```

- `--directory apps/mobile` creates the app inside the `apps/` workspace.
- `--skip-git-init` prevents React Native from creating a nested Git repository.
- `--skip-install` defers dependency installation to Yarn workspaces.

### Installing dependencies from the workspace root

After the project is generated, install all dependencies once from the root of the repository:

```bash
yarn install
```

From this point on, the `yarn install` should be run from the workspace root. Running install commands inside individual app or package folders can break workspace dependencies.

## React Native configuration for monorepos

Out of the box, React Native assumes a single-project setup with all dependencies located in the app’s own `node_modules` directory. In a monorepo, this assumption no longer holds, so we need to adjust the configurations.

### Updating the Metro configuration

At this stage, we need to make a Metro bundler to import code from shared packages while still working with hoisted dependencies. To do that, open `apps/mobile/metro.config.js` and update it as follows:

```javascript
const path = require('path');
const { getDefaultConfig } = require('@react-native/metro-config');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];

module.exports = config;
```

- `watchFolders` tells Metro to watch the entire workspace for changes, including shared packages.
- `nodeModulesPaths` ensures Metro can resolve dependencies hoisted to the workspace root.

### iOS setup

To install CocoaPods, you can use the workspace shortcut from the repository root:

```bash
yarn workspace mobile exec bash -c "cd ios && pod install"
```

- `exec` runs a command inside the workspace environment, with access to its dependencies and binaries.
- `bash -c` allows us to change directories and execute multiple shell commands in one step.

Alternatively, you can navigate to `apps/mobile/ios` and run `pod install` directly from there.

### Android setup

Update the `settings.gradle` file to correctly reference the React Native Gradle plugin from the workspace root:

```plaintext
- 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')
```

And add this to your `android/build.gradle` in `buildscript` section:

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

## TypeScript configuration for a monorepo

In a monorepo setup, it’s useful to share a small set of TypeScript defaults across applications and shared packages. Instead of duplicating the same options in every `tsconfig.json`, we’ll start with a minimal base configuration that others can extend.

First, add TypeScript to the root `package.json` dev dependencies:`‍`

```bash
yarn add -D typescript
```

### Creating a shared base config

Create `tsconfig.base.json` in the project root containing the following:

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node",
    "jsx": "react-jsx",

    "strict": true,
    "strictNullChecks": true,

    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "exclude": ["node_modules"]
}
```

This file defines a small set of language-level defaults that work well for both React Native and web projects. It avoids assumptions about the runtime, bundler, or output format, making it safe to extend across the entire monorepo.

## Setting up shared packages

Sharing code is one of the main reasons for using a monorepo. Instead of duplicating logic or components across applications, we can extract them into shared packages that are consumed by both mobile and web apps.

### Creating the UI package

We’ll start by creating a shared `ui` package that will contain a cross-platform UI component, a simple `Button`.

Prepare the following directory structure:`‍`

```plaintext
packages/ui/
  ├── src/
  │   ├── Button.tsx           # Default implementation
  │   ├── Button.native.tsx    # Mobile override
  │   ├── Button.types.ts      # Shared props types
  │   └── index.ts             # Package entry
  ├── package.json
  ├── tsconfig.json            # TS config for development 
  └── tsconfig.build.json      # TS config for builds
```

Next, create `packages/ui/package.json` with the following config:

```json
{
  "name": "@example/ui",
  "version": "1.0.0",
  "private": true,
  "main": "src/index.ts",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc -p tsconfig.build.json"
  }
}
```

Here we make a package to expose its compiled output and TypeScript types from the `dist` directory.

### Creating the shared UI component

Inside `packages/ui/src/Button.tsx`, create a component you use across web:

```javascript
import { ButtonProps } from './Button.types'

export const Button = ({ label }: ButtonProps) => (
  <button style={{padding: '16px', cursor: 'pointer'}}>
    {label}
  </button>
);
```

Inside `packages/ui/src/Button.native.tsx`, add a React Native component:

```javascript
import { TouchableOpacity, Text } from 'react-native';
import { ButtonProps } from './Button.types'

export const Button = ({ label }: ButtonProps) => (
  <TouchableOpacity>
    <Text>{label}</Text>
  </TouchableOpacity>
);
```

Add shared `ButtonProps` type into the `packages/ui/src/Button.types.ts`:

```javascript
export type ButtonProps = { label: string };
```

Export `Button` component from the package in the `packages/ui/src/index.ts`:

```javascript
export { Button } from './Button';
```

### TypeScript setup for shared packages

Shared packages extend the root `tsconfig.base.json` configuration and define their own development and build settings.

Create `packages/ui/tsconfig.json`:

```javascript
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "noEmit": true,
  },
  "include": ["src"],
  "exclude": ["dist"]
}
```

This configuration is used during development, limiting scope to the `src` directory. The `noEmit: true` ensures TypeScript performs type-checking without generating output. Next, create `packages/ui/tsconfig.build.json`:

```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "declaration": true,
    "declarationMap": false
  },
  "include": ["src"]
}
```

This configuration is used only when building the package and emits only TypeScript declaration files into the `dist` directory. JavaScript output is intentionally skipped, as runtime bundling is handled by the consuming applications. Setting `"noEmit": false` ensures TypeScript emits build artifacts for this package.

Now you need to add a TypeScript dev dependency to the package:

```bash
yarn workspace @example/ui add -D typescript
```

### Building the shared package

The shared package is all set. Run this command from the repository root to build the package:

```bash
yarn workspace @example/ui build
```

Don’t forget to add the `dist/` directory to your `.gitignore` file, as build artifacts should not be committed to the repository.

## Setting up a web project

With the shared packages in place, we can now add a web application that consumes the same code as the React Native app. In this guide, we’ll use Next.js, a natural choice for building modern web applications.

Create a Next.js app and complete the setup steps:

```bash
npx create-next-app@latest apps/web --ts --use-yarn
```

`‍`Add shared`@example/ui` to the web app:

```bash
yarn workspace web add @example/ui
```

Start the web server:

```bash
yarn workspace web dev
```

## Using shared UI across platforms

Now we are ready to verify that the same shared UI component works in both the web and mobile applications.

### Using the shared component on the web

Open `apps/web/src/pages/index.tsx` and import the shared `Button` component:

```javascript
import { Button } from '@example/ui';

export default function Home() {
  return (
    <main className="container">
      <div className="bg-gray-100">
        <Button label="Hello from Web" />
      </div>
    </main>
  );
}
```

### Using the shared component on mobile

We already added the `@example/ui` package to the web, so it’s time to do the same for the mobile app:

```bash
yarn workspace mobile add @example/ui
```

Now, open `apps/mobile/App.tsx` and update it as follows:`‍`

```javascript
import { View, StyleSheet } from 'react-native';
import { Button } from '@example/ui';

export default function App() {
  return (
    <View style={styles.container}>
      <Button label="Hello from Mobile" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```

## `‍`Summary and next steps

We’ve built a working foundation for a React Native monorepo that supports mobile and web applications, with shared UI and logic. From here, you can extend your monorepo in several directions: adding more shared packages, adding more supported platforms, or integrating CI workflows tailored to a multi-app repository.

If you’re interested in how this approach scales in real-world scenarios, you may also find these related articles useful:

- [**Optimize Development Efficiency for Super Apps**](https://www.callstack.com/blog/optimize-development-efficiency-when-building-super-app)
- [**Adding an Example App to Your React Native Library**](https://www.callstack.com/blog/adding-an-example-app-to-your-react-native-library)

Good luck!
