DEV Community

Rita
Rita

Posted on • Originally published at ridev.blog

Share code between React Native and React Web

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, 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]


This article expects that you know how to create yarn workspaces monorepo. If not, please, read docs here.

Also, I have example repo with mobile and web sharing some code.

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
...
Enter fullscreen mode Exit fullscreen mode

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

  ├── projects
+ |   ├── shared
+ |   |   ├── src
+ |   |   ├── index.js
+ |   |   └── package.json
  │   ├── web
  │   └── ...
  ├── package.json
...
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "name": "shared",
  "scripts": {
    ...
  },
  "dependencies": {
    ...
  }
}

Enter fullscreen mode Exit fullscreen mode
// index.js
export { sayHello } from './src/sayHello'

// src/sayHello.js
export function sayHello() {
  return 'Hello world!';
}
Enter fullscreen mode Exit fullscreen mode

2. Add shared to other workspaces

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

// web/package.json
Enter fullscreen mode Exit fullscreen mode
{
  "name": "web",
  "scripts": {
    ...
  },
  "dependencies": {
+   "shared": "*" 
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

// projects/web/src/somefile.ts

import { sayHello } from "shared";

sayHello();
Enter fullscreen mode Exit fullscreen mode

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.

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

  └── projects
      ├── shared
      |   ├── src
      |   ├── index.ts
      |   └── package.json
      ├── web
+     └── mobile
+         |   ├── src
+         |   ...
+         ├── metro.config.js
+         ├── babel.config.js
+         └── package.json
...
Enter fullscreen mode Exit fullscreen mode

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.

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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?

4. Import files from shared into mobile

Remember how we imported it on the web?

import { sayHello } from "shared";
Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

And now we can do imports like this in mobile as well:

import { sayHello } from "shared";
Enter fullscreen mode Exit fullscreen mode

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:

{
  "scripts": {
+   "postinstall": "cd ./projects/mobile && yarn"
  }
}
Enter fullscreen mode Exit fullscreen mode

Minimal working example [repo]


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

Top comments (0)