Share code between React Native and React Web
Tags: react, reactnative, webdev, monorepo
Published: 2022-12-14 22:55:16


This article explains a coexisting of react native and react web apps with similar to monorepo workspaces behavior, but without sharing `node_modules` dependencies. 

**All web workspaces remain workspaces, mobile just _acts_ like workspace.**

I've had an existing huge web monorepo. [Yarn's `nohoist` setup](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/), or editing metro configuration wasn't working for me because this monorepo contains a lot of dependencies incompatible with react native. **But it might work for you** and might be a better solution.

## TL;DR
1. You need to update metro config to handle sources outside of react native project.
2. Setup mobile project's babel config with aliases to shared folders.
3. Enjoy the magic ✨

[Minimal working example [repo]](https://github.com/romenkova/react-native-monorepo-example)

---

This article expects that you know how to create yarn workspaces monorepo. If not, please, [read docs here](https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/).

Also, I have [example repo](https://github.com/romenkova/react-native-monorepo-example) with mobile and web sharing some code.

## <a name="1-create-folder-which-needs-to-be-shared"></a>[1. Create folder which needs to be shared](#1-create-folder-which-needs-to-be-shared)
Let's say, we have our workspaces in the folder `projects`, like this:
```
├── projects
│   ├── web
│   └── ...  (means here can be lots of files)
├── package.json
...
```

Add a new workspace, which will contain shared files for both mobile and web.

```diff
  ├── projects
+ |   ├── shared
+ |   |   ├── src
+ |   |   ├── index.js
+ |   |   └── package.json
  │   ├── web
  │   └── ...
  ├── package.json
...
```

```json
// package.json
{
  "name": "shared",
  "scripts": {
    ...
  },
  "dependencies": {
    ...
  }
}

```
```js
// index.js
export { sayHello } from './src/sayHello'

// src/sayHello.js
export function sayHello() {
  return 'Hello world!';
}
```

## <a name="2-let-other-web-workspaces-consume-shared-folder"></a>[2. Add `shared` to other workspaces](#2-let-other-web-workspaces-consume-shared-folder)

Now we need to add our shared folder into `package.json` files of other workspaces.

```json
// web/package.json
```
```diff
{
  "name": "web",
  "scripts": {
    ...
  },
  "dependencies": {
+   "shared": "*" 
    ...
  }
}
```
and same of all other workspaces, where we want to use `shared`.
Then, to let yarn know what we did, run `yarn install` or just `yarn`.

Now we can do something like this:

```ts
// projects/web/src/somefile.ts

import { sayHello } from "shared";

sayHello();
```

## <a name="3-create-react-native-application"></a>[3. Create react native application](#3-create-react-native-application)

React Native setup guide does a better job at explaining how to set up a new mobile app, or maybe you have your existing one. I'll skip this part, if you're not sure how to do this, [docs are here](https://reactnative.dev/docs/environment-setup).

You need to create a mobile app inside `projects` folder like this:

```diff
  └── projects
      ├── shared
      |   ├── src
      |   ├── index.ts
      |   └── package.json
      ├── web
+     └── mobile
+         |   ├── src
+         |   ...
+         ├── metro.config.js
+         ├── babel.config.js
+         └── package.json
...
```

**And now the most important part**.
We need to tell the metro where to find source files. If we don't do that, it'll only look inside the `mobile` folder.

```js
const path = require("path");

const projectRoot = __dirname;

// store path to workspace root, in our case, one level above /projects
const workspaceRoot = path.resolve(projectRoot, "../../");

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },

  // watch not only our mobile folder, but also root's folders
  // this will include @common
  watchFolders: [path.resolve(__dirname, workspaceRoot)],
  resolver: {
    nodeModulesPaths: [
      // Tell metro to resolve modules in /mobile folder
      // and if not found, then try to find modules in workspace
      path.resolve(projectRoot, "node_modules"),
      path.resolve(workspaceRoot, "node_modules"),
    ],
    // And to make thing above work:
    disableHierarchicalLookup: true,
  },
};
```

So what happens above:
1. we tell metro to look for files everywhere (meaning in root dir)
2. resolve `node_modules` from workspace root as well. 

Second part is needed, because at some point `shared` code will use some packages, and they will be in root's `node_modules`

Okay, now metro sees codes from the `shared` folder, how to import them into a mobile project?

## <a name="4-import-files-from-shared-folder-into-mobile"></a>[4. Import files from `shared` into mobile](#4-import-files-from-shared-folder-into-mobile)

Remember how we imported it on the web?
```ts
import { sayHello } from "shared";
```
It is possible, because `shared` is a workspace. It's like having yet another node module.

But `mobile` doesn't treat it as a workspace, we will import files directly. And to have similar experience as in web, we can use babel aliases:

`yarn add -D babel-plugin-module-resolver`

then 

```js
// mobile/babel.config.js
module.exports = {
  presets: ["module:metro-react-native-babel-preset"],
  plugins: [
    [
      "module-resolver",
      {
        root: ["./"],
        alias: {
          "shared": "../shared", // needed alias
        },
      },
    ],
  ],
};

```

And now we can do imports like this in mobile as well:
```ts
import { sayHello } from "shared";
```
---

At this point you'll be able to run mobile and web, and see how changes in `shared` code reflect on both.

I also recommend to add this line to root `package.json`, to have mobile packages installed from `yarn` command:
```diff
{
  "scripts": {
+   "postinstall": "cd ./projects/mobile && yarn"
  }
}
```

[Minimal working example [repo]](https://github.com/romenkova/react-native-monorepo-example)

---

Do you see any downsides to this approach? Please, let me know in the comments.