The Farrelly logo

React/React-native monorepo with yarn workspaces tutorial

Monorepos are a great tool to use when you have code to share across many different repositories. For example, if you have a web, and native app which can share code, creating a ‘common’ or ‘shared’ repository can keep your code DRY. Otherwise you’re going to be abusing copy and paste in a way Larry Tesler won’t be proud of.

Google, Facebook, Microsoft, and all other tech giants use monorepos to organise, and run their systems. Goliath sized companies aren’t the only ones who do, or stand to gain from the technology.

Any software, or platform which has many distinct parts can be split up in this way to separate concerns, and maintain DRY, focused code. Some of the most popular React libraries are split up this way, see ‘react-router’ as an example.

There are many tools which can handle a monorepo setup, all with their own distinct advantages. Yarn workspaces, Lerna, Bazel are examples of some of the most well-known tools for the job.

But how do you set up a monorepo? And what does it look like with a React web, and native app?

Want to skip ahead and see the finished result? Check the repo out here.

What you will need

  • node
  • yarn
  • react/react-native environment setup

I’m using node v14, and yarn v1.22. You can use different versions, but your results may vary somewhat.

Now let’s create a React, React-native monorepo with yarn workspaces!

To begin

Create the directory for our monorepo. I’m going to name mine ‘react-monorepo-example’, but name yours whatever makes sense.

Inside our new directory, let’s create our package.json, to do so

yarn init

Update the newly created package.json with the following properties

"workspaces": {
    "packages": [
        "packages/**"
    ]
}

The “workspaces” key turns on the yarn workspaces feature, while the “packages” key defines which directories are considered packages by the root directory.

So in our example here, once we create the web, shared, and app folders within the packages directory all of those will be considered packages by the root.

So lets create each of our packages, first lets create the packages directory

mkdir packages && cd packages

Now let’s create each of our packages

Web

npx create-react-app web --template typescript

App

npx react-native init app --template react-native-template-typescript

Shared

npx create-react-app shared --template typescript

(The shared directory uses React to keep logic similar, and take advantage of key tools such as the React lifecycle for business logic)

Note, you don’t have to use typescript for a monorepo to work, or even to complete this tutorial. I just prefer using typescript, and that’s why I’ve included it here.

Setting up hoisting

Yarn workspaces has a great feature called hoisting - in brief, if you use the same library across multiple packages (example being React), yarn workspaces will ‘hoist’ those dependencies to the root ‘node_modules’ directory. All sub-projects which use that same dependency will access the ‘hoisted’ version at the root directory.

Since we’re using React across each of these projects, using the same hoisted version will save us from many issues which can arise if you choose to exclude it. Personally I spent a nightmarish couple days trying to stop seeing a red screen saying ‘Multiple instances of React found’.

Luckily for us, only the React Native app requires some hoisted libraries to be ignored. If you’re also using the ‘react-native-template-typescript’ then you can follow exactly what i’ve ignored here

  "workspaces": {
    "nohoist": [
      "**/react-native",
      "**/react-native/**",
      "jetifier",
      "react-native-safe-area-context",
      "react-native-screens",
      "@react-native-community/**",
      "@react-navigation/**"
    ]
  },

The app, and shared directory don’t require any ‘nohoist’ settings to be configured.

Name change (optional)

While you’ve got each ‘package.json’ open, you have the option to rename what the package will be accessed as by other packages. For example we currently have a ‘shared’ package, which will be accessed by other packages (in future) by accessing “shared/{path}“.

To make it more distinct and obvious that it’s a part of our monorepo setup we could rename it within the ‘package.json’. A common pattern is to rename a package by inserting an @ sign, along with the monorepo project name. So for example our “shared” could become “@react-monorepo-example/shared” instead. Hopefully your project isn’t as much of a mouthful.

Although this is optional, I’ve opted to do it in this tutorial as it does make the import path more distinct.

{
  "name": "@react-monorepo-example/shared",
  "version": "0.0.1",
  ...,
}

Linking the packages

Let’s now set up the shared package to be consumed by the app, and the web package. To do this we need to do two things, add it as a dependency within our ‘package.json’, and to ‘yarn link’ the directories.

So in our example we would add ‘@react-monorepo-example/shared’ to dependencies for both the app, and web. When adding the dependency make sure the version number of the “shared” directory is the same as the version value within the shared folders ‘package.json’.

Web

  {
    ...,
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1",
    "@react-monorepo-example/shared": "0.0.1"
  },

App

"dependencies": {
    "react": "17.0.2",
    "react-native": "0.66.3",
    "@react-monorepo-example/shared": "0.0.1"
  },

Let’s now ‘yarn link’ the directories. Change into the shared directory and run

yarn link

Now let’s change into our other packages (web, app), and run

yarn link "@react-monorepo-example/shared"

Now we’ve successfully set up each package, added them as a dependency, and linked them.

To be cautious since we’ve adjusted our ‘nohoist’ settings, and haven’t yarn installed our shared package yet, we should delete the root ‘node_modules’, and ‘yarn.lock’ as it can cache changes (leading to some hair pulling configuration issues).

From the root directory,

rm -rf node_modules yarn.lock packages/*/node_modules

Fresh install, who dis

Now we need to install our new dependencies. One nifty feature from yarn workspaces is that when you run yarn install in the root, or any project directory, it will install all required dependencies for all packages. So app, web, and shared will all update.

To take advantage of that, and kill two birds with one stone, let’s change directory to the app package, and yarn from there. That way we can yarn install, and do a pod install

yarn && cd ios && pod install && cd ../

Cleaning up the shared directory

Now we’re onto the last-leg of this tutorial. Let’s first do some clean-up and remove unneeded dependencies from the shared package we only need React, and Typescript.

The shared directory’s ‘package.json’ should look like this

{
  "name": "@react-monorepo-example/shared",
  "version": "0.0.1",
  "private": true,
  "workspaces": {
    "nohoist": []
  },
  "dependencies": {
    "react": "^17.0.2",
    "typescript": "^4.1.2"
  }
}

Lets also remove all the files within the ‘public’ and ‘src’ directories - since it’s just a shared code space we don’t need an actual running React application.

While we’re within the shared folder let’s set up a value to be shared across the app, and web. Create a new file called ‘test.ts’, and let’s put an exported ‘testNumber’ with a set value there.

export const testNumber = 50;

Our next step is to setup the app, and see our shared value come through.

React-native setup

Before we start our metro server, we need to update it to watch the ‘shared’ folder for any changes. So let’s open up the ‘metro.config.js’, and insert

watchFolders: [
    // watch for shared folder changes
    path.resolve(__dirname, '../shared'),
],

our metro.config.js file

Now we can start our metro server

yarn start

Next we need to start the app. Lets do that by either running

// from the CLI
yarn ios
// or
yarn android

Alternatively by opening Xcode/Android Studio. Personally, I generally always start React Native apps via opening the ‘.xcworkspace’ file within the ‘app/ios/app/’ path inside of Xcode.

Next let’s add some text within the ‘App.tsx’ which will reference the shared value we made earlier

<Text>And our test number is {testNumber}</Text>

Make sure the import to ‘testNumber’ is absolute, and not relative, it should look like this

import {testNumber} from "@react-monorepo-example/shared/test"

If the above instructions have worked, you should see this on your machine

How our app should now look

Now let’s also make sure that updates to the shared folder also update for our metro server, so let’s change the ‘testNumber’ from 50, to 60. If it worked, it should look like this.

How the updated test number should look in the app

If we didn’t update our ‘metro.config.js’ to watch the shared folder you would need to manually restart the metro server every time you changed something in the shared directory.

Web setup

Lucky for us, the web package should work straight out of the box. Let’s just update the ‘App.tsx’ to use that shared value as well.

Let’s insert

<p>And our test number is: {testNumber}</p>

And make sure the import path is absolute, it should be the same as the import we used for the app!

Then to launch the web app, within it’s directory just run

yarn start

How the web app should look

Even hot reload works straight out of the box! Update the testNumber back down to 50!

How the updated test number should look in the app

Recap

If you’ve followed along, congratulations! You’ve setup your first React monorepo! 👏

You’ve achieved the following

  • Setup React web app
  • Setup React native app
  • Created monorepo structure
  • Linked ‘shared’ repository to both apps
  • Saw a shared value appear across both platforms

The skies the limit!

The ‘testNumber’ is just a placeholder for every other thing you can imagine sharing between the two code bases. Perhaps you want to share hooks, images, brand colours, you name it.

If you didn’t follow along, you can check out the finished product here.