Tutorial: How to share code between iOS, Android & Web using React Native, react-native-web and monorepo
Tags: react, reactnative, webdev, javascript
Published: 2019-03-06 18:53:37

---
title: Tutorial: How to share code between iOS, Android & Web using React Native, react-native-web and monorepo
published: true
description: How to make react-native work in the browser, the right way 
cover_image: https://thepracticaldev.s3.amazonaws.com/i/xud2e2pmwxfelfqel53x.jpg
tags: react, reactnative, webdev, javascript
---

Let's make our `react-native` app work in the browser, the right way.


> This tutorial was made for `react-native <= 0.61`. If you are using a newer version, I recommend that you fork this repository instead: [brunolemos/react-native-web-monorepo](https://github.com/brunolemos/react-native-web-monorepo), I am keeping it updated 🙌

---

## Why am I writing this?

Hi 👋 I'm [Bruno Lemos](https://twitter.com/brunolemos). I recently launched a project called [DevHub - TweetDeck for GitHub](http://github.com/devhubapp/devhub) and one of the things that [caught people's attention](https://twitter.com/brunolemos/status/1072871009651384320?s=21) was the fact that it is an app made by a single developer and available on 6 platforms: Web (react-native-web), iOS (`react native`), Android (`react native`), macOS, Windows and Linux (`electron`, _for now_), with almost 100% code sharing between them. It even shares some code with the server! This is something that would require a team of 3+ until a couple years ago.

Since then, I've received dozens of tweets and private messages asking how to achieve the same and in this tutorial I'll walk you through it.




## What's `react-native-web`?

If you are not familiar with [react-native-web](http://github.com/necolas/react-native-web), it's a lib by [Necolas](https://twitter.com/necolas) (ex Twitter engineer) to make your `React Native` code render in the browser. Roughly speaking, you will write `<View />` and it will render `<div />`, making sure all styles render the exact same thing. It does more than that, but let's keep it simple.

The [new Twitter](https://mobile.twitter.com/) was created using this technology and it's awesome.

If you already know `react-native`, you don't need to learn any new syntax. It's the [same API](https://github.com/necolas/react-native-web#components).

---

## Summary


  - Starting a new `React Native` project
  - Turning our folder structure into a monorepo
  - Making `react-native` work in a monorepo
  - Sharing code between our monorepo packages
  - Creating a new web project using `create-react-app` and `react-native-web`
  - Making `CRA` work inside our `monorepo` with code sharing
  - ???
  - Profit

---


## Step-by-step tutorial

### Starting a new `React Native` project

- `$ react-native init myprojectname`
- `$ cd myprojectname`
- `$ git init && git add . -A && git commit -m "Initial commit"`

> Note: It's much easier to create a cross platform app from scratch than trying to port an existing mobile-only (or even harder: web-only) project, since they may be using lot's of platform specific dependencies.

> EDIT: If you use expo, it seems they will soon have [built in support for web](https://twitter.com/brunolemos/status/1105493974285017090?s=20)!



### Turning our folder structure into a monorepo

Monorepo means having multiple packages in a single repository so you can easily share code between them.  It's a bit less trivial than it sounds because both `react-native` and `create-react-app` require some work to support monorepo projects. But hey, at least it's possible!

We'll use a feature called `Yarn Workspaces` for that.
Requirements: [Node.js](https://nodejs.org/en/), [Yarn](https://yarnpkg.com/lang/en/docs/install/) and [React Native](https://facebook.github.io/react-native/docs/getting-started.html). 

  - _Make sure you are at the project root folder_
  - `$ rm yarn.lock && rm -rf node_modules`
  - `$ mkdir -p packages/components/src packages/mobile packages/web`
  - Move all the files (except `.git`) to the `packages/mobile` folder
  - Edit the `name` field on `packages/mobile/package.json` from `packagename` to `mobile`
  - Create this `package.json` at the root directory to enable `Yarn Workspaces`:

```json
{
  "name": "myprojectname",
  "private": true,
  "workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": []
  }
  "dependencies": {
    "react-native": "0.61.3"
  }
}
```


  - Create a `.gitignore` at the root directory:

```
.DS_Store
.vscode
node_modules/
yarn-error.log
```

  - `$ yarn`



### Making react-native work in a monorepo

- _Check where `react-native` got installed. If it was at `/node_modules/react-native`, all right. If it was at `/packages/mobile/node_modules/react-native`, something is wrong. Make sure you have the latest versions of `node` and `yarn`. Also make sure to use the exact same version of dependencies between the monorepo packages, e.g. `"react": "16.11.0"` on both `mobile` and `components`, not a different version between them._

- Open your favorite editor and use the `Search & Replace` feature to replace all occurrences of `node_modules/react-native/` with `../../node_modules/react-native/`.

- For react-native <= 0.59, open `packages/mobile/package.json`. Your `start` script currently ends in `/cli.js start`. Append this to the end: `--projectRoot ../../`.

- Open `packages./mobile/metro.config.js` and set the `projectRoot` field on it as well so it looks like this:

```js
const path = require('path')

module.exports = {
  projectRoot: path.resolve(__dirname, '../../'),
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
}
```

- [Workaround] You currently need to add the `react-native` dependency to the root `package.json` to be able to bundle the JS:

```json
  "dependencies": {
    "react-native": "0.61.3"
  },
```


#### iOS changes

  - `$ open packages/mobile/ios/myprojectname.xcodeproj/`
  - Open `AppDelegate.m`, find `jsBundleURLForBundleRoot:@"index"` and replace `index` with `packages/mobile/index`
  - Still inside Xcode, click on your project name on the left, and then go to `Build Phases` > `Bundle React Native code and Images`. Replace its content with this:

```shell
export NODE_BINARY=node
export EXTRA_PACKAGER_ARGS="--entry-file packages/mobile/index.js"
../../../node_modules/react-native/scripts/react-native-xcode.sh
```
  - `$ yarn workspace mobile start`

You can now run the iOS app! 💙 Choose one iPhone emulator and press the "Run" triangle button inside Xcode.

![image](https://thepracticaldev.s3.amazonaws.com/i/hw74nbu4d0h49kh90qmv.png)

#### Android changes

  - `$ studio packages/mobile/android/`
  - Open `packages/mobile/android/app/build.gradle`. Search for the text `project.ext.react = [...]`. Edit it so it looks like this:

```gradle
project.ext.react = [
    entryFile: "packages/mobile/index.js",
    root: "../../../../"
]
```

  - _Android Studio will show a Sync Now popup. Click on it._
  - Open `packages/mobile/android/app/src/main/java/com/myprojectname/MainApplication.java`. Search for the `getJSMainModuleName` method. Replace `index` with `packages/mobile/index`, so it looks like this:

```java
@Override
protected String getJSMainModuleName() {
  return "packages/mobile/index";
}
```

> If you get the `Cannot get property 'packageName' on null object` error, try [disabling auto linking](https://github.com/react-native-community/cli/issues/655)

You can now run the Android app! 💙 Press the "Run" green triangle button inside Android Studio and choose the emulator or device.

![image](https://thepracticaldev.s3.amazonaws.com/i/zhpmuclw1ekywgu5fw3t.png)


### Sharing code between our monorepo packages

We've created lots of folders in our monorepo, but only used `mobile` so far. Let's prepare our codebase for code sharing and then move some files to the `components` package, so it can be reused by `mobile`, `web` and any other platform we decide to support in the future (e.g.: `desktop`, `server`, etc.).

  - Create the file `packages/components/package.json` with the following contents:

```json
{
  "name": "components",
  "version": "0.0.1",
  "private": true
}
```

  - _[optional] If you decide to support more platforms in the future, you'll do the same thing for them: Create a `packages/core/package.json`, `packages/desktop/package.json`, `packages/server/package.json`, etc. The name field must be unique for each one._

  - Open `packages/mobile/package.json`. Add all the monorepo packages that you are using as dependencies. In this tutorial, `mobile` is only using the `components` package:

```json
"dependencies": {
  "components": "0.0.1",
  ...
}
```

  - _Stop the react-native packager if it's running_
  - `$ yarn`
  - `$ mv packages/mobile/App.js packages/components/src/`
  - Open `packages/mobile/index.js`. Replace `import App from './App'` with `import App from 'components/src/App'`. _This is the magic working right here. One package now have access to the others!_
  - Edit `packages/components/src/App.js`, replace `Welcome to React Native!` with `Welcome to React Native monorepo!` so we know we are rendering the correct file.
  - `$ yarn workspace mobile start`
  
 Yay! You can now refresh the running iOS/Android apps and see our screen that's coming from our shared components package. 🎉

  - `$ git add . -A && git commit -m "Monorepo"`



## Web project

> Note: You can reuse up to 100% of the code, but that doesn't mean you should. It's recommended to have some differences between platforms to make them feel more natural to the user. To do that, you can create platform-specific files ending with `.web.js`, `.ios.js`, `.android.js` or `.native.js`. See [example](https://github.com/devhubapp/devhub/tree/v0.90.0/packages/components/src/libs/bugsnag).

### Creating a new web project using CRA and react-native-web

  - `$ cd packages/`
  - `$  npx create-react-app web`
  - `$ cd ./web` _(stay inside this folder for the next steps)_
  - `$ rm src/*` _(or manually delete all files inside `packages/web/src`)_
  - _Make sure the dependencies inside `package.json` are the exact same between all monorepo packages. For example, update the "react" version to "16.9.0" (or any other version) on both `web` and `mobile` packages._
  - `$ yarn add react-native-web react-art`
  - `$ yarn add --dev babel-plugin-react-native-web`
  - Create the file `packages/web/src/index.js` with the following contents:

```jsx
import { AppRegistry } from 'react-native'

import App from 'components/src/App'

AppRegistry.registerComponent('myprojectname', () => App)
AppRegistry.runApplication('myprojectname', {
  rootTag: document.getElementById('root'),
})
```

> Note: when we import from `react-native` inside a `create-react-app` project, its `webpack` config automatically [alias it to `react-native-web`](https://github.com/facebook/create-react-app/blob/ff19e0a6d22ba070f5b4c3b511edb4e8d4995686/packages/react-scripts/config/webpack.config.js#L274-L278) for us.

  - Create the file `packages/web/public/index.css` with the following contents:

```css
html,
body,
#root,
#root > div {
  width: 100%;
  height: 100%;
}

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
```

  - Edit `packages/web/public/index.html` to include our CSS before closing the `head` tag:

```html
...
<title>React App</title>
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" />
</head>
```

### Making CRA work inside our monorepo with code sharing

CRA doesn't build files outside the `src` folder by default. We need to make it do it, so it can understand the code from our monorepo packages, which contains JSX and other non-pure-JS code.

  - _Stay inside `packages/web/` for the next steps_
  - Create a `.env` file (`packages/web/.env`) with the following content:

```
SKIP_PREFLIGHT_CHECK=true
```

  - `$ yarn add --dev react-app-rewired`
  - Replace the scripts inside `packages/web/package.json` with this:

```json
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-app-rewired eject"
},
```

  - Create the `packages/web/config-overrides.js` file with the following contents: 

```js
const fs = require('fs')
const path = require('path')
const webpack = require('webpack')

const appDirectory = fs.realpathSync(process.cwd())
const resolveApp = relativePath => path.resolve(appDirectory, relativePath)

// our packages that will now be included in the CRA build step
const appIncludes = [
  resolveApp('src'),
  resolveApp('../components/src'),
]

module.exports = function override(config, env) {
  // allow importing from outside of src folder
  config.resolve.plugins = config.resolve.plugins.filter(
    plugin => plugin.constructor.name !== 'ModuleScopePlugin'
  )
  config.module.rules[0].include = appIncludes
  config.module.rules[1] = null
  config.module.rules[2].oneOf[1].include = appIncludes
  config.module.rules[2].oneOf[1].options.plugins = [
    require.resolve('babel-plugin-react-native-web'),
  ].concat(config.module.rules[2].oneOf[1].options.plugins)
  config.module.rules = config.module.rules.filter(Boolean)
  config.plugins.push(
    new webpack.DefinePlugin({ __DEV__: env !== 'production' })
  )

  return config
}
```

> The code above overrides some `create-react-app`'s `webpack` config so it includes our monorepo packages in CRA's build step

- `$ git add . -A && git commit -m "Web project"`

That's it! You can now run `yarn start` inside `packages/web` (or `yarn workspace web start` at the root directory) to start the web project, sharing code with our `react-native` `mobile` project! 🎉


![image](https://thepracticaldev.s3.amazonaws.com/i/48ij2zssunapdrb4qnog.jpg)

## Some gotchas

  - `react-native-web` supports most of the `react-native` API, but a few pieces are missing like `Alert`, `Modal`, `RefreshControl` and `WebView`;
  - If you come across a dependency that doesn't work well with the monorepo structure, you may add it to the [nohoist](https://yarnpkg.com/blog/2018/02/15/nohoist/) list; But avoid that if possible, because it may cause other issues, specially with the metro bundler.

## Some tips

  - Navigation may be a bit of a challenge; you can use something like [react-navigation](https://github.com/react-navigation/react-navigation) which recently added web support or you can try using two different navigators between and mobile, in case you want the best of both worlds by compromising some code sharing;
  - If you plan sharing code with the server, I recommend creating a `core` package that only contain logic and helper functions (no UI-related code);
  - For Next.js, you can check their [official example with react-native-web](https://github.com/zeit/next.js/tree/master/examples/with-react-native-web)
  - For native windows, you can try [react-native-windows](https://github.com/Microsoft/react-native-windows);
  - For native macOS, you can the new Apple Project Catalyst, but support for it is not 100% there yet ([see my tweet](https://twitter.com/brunolemos/status/1185636022346043392?s=20));
  - To install new dependencies, use the command `yarn workspace components add xxx` from the root directory. To run a script from a package, run `yarn workspace web start`, for example; To run a script from all packages, run `yarn workspaces run scriptname`;



Thanks for reading! 💙

If you like react, consider following me here on Dev.to and on [Twitter](https://twitter.com/brunolemos).

---


#### Links

- Source code: [react-native-web-monorepo](https://github.com/brunolemos/react-native-web-monorepo)
- DevHub: [devhubapp/devhub](http://github.com/devhubapp/devhub) (production app using this structure + Desktop + TypeScript)
- Twitter: [@brunolemos](https://twitter.com/brunolemos)