Running React Native everywhere: Android & iOS
Tags: javascript, webdev, react, reactnative
Published: 2021-09-21 12:21:57

---
title: Running React Native everywhere: Android & iOS
published: true
date: 2021-09-21 12:21:00 UTC
tags: #javascript #webdev #react #reactnative
canonical_url: https://mmazzarolo.com/blog/2021-09-18-running-react-native-everywhere-mobile/
cover_url: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cunw376pndw2jx88a9n.png
---

## TL;DR

Second part of the ["Running React Native everywhere"](https://dev.to/mmazzarolo/running-react-native-everywhere-4gpp) series: a tutorial about structuring your project to run multiple React Native apps targeting different platforms.

This time, we'll build a modular React Native app using a Yarn Workspaces monorepo, starting from Android & iOS.

## The next step

Now that the monorepo foundation is in place, we can start building our app.  
The next step is encapsulating **the shared React Native code** and **the native Android & iOS code** in two different workspaces:

```bash
.
└── <project-root>/
    └── packages/
        # React Native JavaScript code shared across the apps
        ├── app/
        │   ├── src/
        │   └── package.json
        # Android/iOS app configuration files and native code
        └── mobile/
            ├── android/
            ├── ios/
            ├── app.json
            ├── babel.config.js
            ├── index.js
            ├── metro.config.js
            └── package.json
```

## The shared React Native JavaScript code: `packages/app`

Lets' start from the shared React Native JavaScript code.  
The idea here is to isolate the JavaScript code that runs the app in an `app` workspace.  
We should think about this workspaces as a standard npm library that can work in isolation.  
So it will have its own `package.json` where we'll explicitly declare its dependencies.

Let's start by creating the new package directory:

```bash
mkdir packages/app && cd packages/app
```

And its `package.json`:

```json
{
  "name": "@my-app/app",
  "version": "0.0.0",
  "private": true,
  "main": "src",
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  }
}
```

As we already explained in the monorepo setup, we're setting `react` and `react-native` as `peerDependencies` because we expect each app that depends on our package to provide their versions of these libraries.

Then, let's create a tiny app in `src/app.js`:

```js
import React from "react";
import {
  Image,
  Platform,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from "react-native";
import LogoSrc from "./logo.png";

export function App() {
  return (
    <SafeAreaView style={styles.root}>
      <Image style={styles.logo} source={LogoSrc} />
      <Text style={styles.text}>Hello from React Native!</Text>
      <View style={styles.platformRow}>
        <Text style={styles.text}>Platform: </Text>
        <View style={styles.platformBackground}>
          <Text style={styles.platformValue}>{Platform.OS}</Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  root: {
    height: "100%",
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "white",
  },
  logo: {
    width: 120,
    height: 120,
    marginBottom: 20,
  },
  text: {
    fontSize: 28,
    fontWeight: "600",
  },
  platformRow: {
    marginTop: 12,
    flexDirection: "row",
    alignItems: "center",
  },
  platformValue: {
    fontSize: 28,
    fontWeight: "500",
  },
  platformBackground: {
    backgroundColor: "#ececec",
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: "#d4d4d4",
    paddingHorizontal: 6,
    borderRadius: 6,
    alignItems: "center",
  },
});

export default App;
```

Thanks to Yarn Workspaces, we can now use `@my-app/app` in any other worskpace by:


- Marking `@my-app/app` as a dependency
- Importing `App`: `import App from "@my-app/app";`


## The native mobile code and configuration

Now that the shared React Native code is ready let's create `packages/mobile`. This workspace will store the Android & iOS native code and import & run `packages/app`.

Using [React Native CLI](https://reactnative.dev/docs/environment-setup), bootstrap a new React Native app within the `packages` directory.

```
cd packages && npx react-native init MyApp && mv MyApp mobile
```

> React Native CLI requires a pascal case name for the generated app — which is why we're using `MyApp` and then renaming the directory to `mobile`.

Then, update the generated `package.json` by setting the new package name and adding the `@my-app/app` dependency:

```diff
 {
-  "name": "MyApp",
+  "name": "@my-app/mobile",
   "version": "0.0.1",
   "private": true,
   "scripts": {
     "android": "react-native run-android",
     "ios": "react-native run-ios",
     "start": "react-native start",
     "test": "jest",
     "lint": "eslint ."
   },
   "dependencies": {
+    "@my-app/app": "*",
     "react": "17.0.2",
     "react-native": "0.65.1"
   },
   "devDependencies": {
     "@babel/core": "^7.12.9",
     "@babel/runtime": "^7.12.5",
     "babel-jest": "^26.6.3",
     "eslint": "7.14.0",
     "get-yarn-workspaces": "^1.0.2",
     "jest": "^26.6.3",
     "metro-react-native-babel-preset": "^0.66.0",
     "react-native-codegen": "^0.0.7",
     "react-test-renderer": "17.0.2"
   },
   "jest": {
     "preset": "react-native"
   }
 }
```

Finally, update `packages/mobile/index.js` to use `@my-app/app` instead of the app template shipped with React Native:

```diff
 import { AppRegistry } from "react-native";
-import App from "./App";
+import App from "@my-app/app";
 import { name as appName } from "./app.json";

 AppRegistry.registerComponent(appName, () => App);
```

## Updating the `nohoist` list

We should be ready to run the app now, right?  
Well... kinda. We still need to update the `nohoist` section of the root `package.json` to include all the libraries required by React Native.

To understand why we need to do so, try installing the iOS pods:

```bash
cd packages/mobile/ios && pod install
```

The command will fail with an error like this:

```bash
[!] Invalid Podfile file: cannot load such file:/Users/me/workspace/react-native-universal-monorepo -> js/packages/mobile/node_modules/@react-native-community/cli-platform-ios/native_modules.
```

As we explained in the previous post, by default Yarn Workspaces will install the dependencies of each package (`app`, `mobile`, etc.) in `<project-root>/node_modules` (AKA "hoisting").  
This behaviour doesn't work well with React Native, because **the native code located in `mobile/ios` and `mobile/android` in some cases references libraries from `mobile/node_modules` instead of `<project-root>/node_modules`**.  
Luckily, we can opt-out of Yarn workspaces' hoisting for specific libraries by adding them to the `nohoist` setting in the root `package.json`:

```diff
 {
   "name": "my-app",
   "version": "0.0.1",
   "private": true,
   "workspaces": {
     "packages": [
       "packages/*"
     ],
     "nohoist": [
       "**/react",
       "**/react-dom",
+      "**/react-native",
+      "**/react-native/**"
     ]
   }
 }
```

Adding the libraries from the diff above should be enough to make an app bootstrapped with React Native 0.65 work correctly:

- `**/react-native` tells Yarn that the `react-native` library should not be hoisted.
- `**/react-native/**` tells Yarn that the all `react-native`'s dependencies (e.g., `metro`, `react-native-cli`, etc.) should not be hoisted.

> You **can** completely opt-out from hoisting on all libraries (e.g., with `"nohoist": ["**/**"]`), but I wouldn't advise doing so unless you feel like maintaining the list of hoisted dependencies becomes a burden.

Once you've updated the `nohoist` list, run `yarn reset && yarn` from the project root to re-install the dependencies using the updated settings.

Now `cd packages/mobile/ios && pod install` should install pods correctly.

## Making metro bundler compatible with Yarn workspaces

Before we can run the app, we still need do one more thing: make [metro bundler](https://facebook.github.io/metro/) compatible with Yarn workspaces' hoisting.

Metro bundler is the JavaScript bundler currently used by React Native.  
One of metro's most famous limitations (and [issue number #1 in its GitHub repository](https://github.com/facebook/metro/issues/1)) is its **inability to follow symlinks**.  
Therefore, since all hoisted libraries (basically all libraries not specified in the `nohoist` list) are installed in `mobile/node_modules` as symlinks from `<root>/node_modules`, metro won't be able to detect them.  
Additionally, because of this issue, **metro won't even be able to resolve other workspaces (e.g., `@my-app/app`) since they're outside of the `mobile` directory**.

For example, running the app on iOS will now show the following (or a similar) error:

```
error: Error: Unable to resolve module @babel/runtime/helpers/interopRequireDefault from /Users/me/workspace/react-native-universal-monorepo-js/packages/mobile/index.js: @babel/runtime/helpers/interopRequireDefault could not be found within the project or in these directories:
  node_modules
```

In this specific case, metro is telling us that he's unable to find the `@babel/runtime` library in `mobile/node_modules`. And rightfully so: `@babel/runtime` is not part of our `nohoist` list, so it will probably be installed in `<root>/node_modules` instead of `mobile/node_modules`.

Luckily, we have [several metro configuration options at our disposal](https://facebook.github.io/metro/docs/configuration) to fix this problem.

With the help of a couple of tools, **we can update the metro configuration file (`mobile/metro.config.js`) to make metro aware of `node_modules`directories available outside of the `mobile` directory** (so that it can resolve `@my-app/app`)... with the caveat that **libraries from the `nohoist` list should always be resolved from `mobile/node_modules`**.

To do so, install [`react-native-monorepo-tools`](https://github.com/mmazzarolo/react-native-monorepo-tools), a set of utilities for making metro compatible with Yarn workspaces based on our `nohoist` list.

```bash
yarn add -D react-native-monorepo-tools
```

And update the metro config:

```diff
 const path = require("path");
 const exclusionList = require("metro-config/src/defaults/exclusionList");
 const { getMetroConfig } = require("react-native-monorepo-tools");

+const yarnWorkspacesMetroConfig = getMetroConfig();

 module.exports = {
   transformer: {
     getTransformOptions: async () => ({
       transform: {
         experimentalImportSupport: false,
         inlineRequires: false,
       },
     }),
   },
+  // Add additional Yarn workspace package roots to the module map.
+  // This allows importing importing from all the project's packages.
+  watchFolders: yarnWorkspacesMetroConfig.watchFolders,
+  resolver: {
+    // Ensure we resolve nohoist libraries from this directory.
+    blockList: exclusionList(yarnWorkspacesMetroConfig.blockList),
+    extraNodeModules: yarnWorkspacesMetroConfig.extraNodeModules,
+  },
 };
```

Here's how the new settings look like under-the-hood:

```js
const path = require("path");
const exclusionList = require("metro-config/src/defaults/exclusionList");
const { getMetroConfig } = require("react-native-monorepo-tools");

const yarnWorkspacesMetroConfig = getMetroConfig();

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  // Add additional Yarn workspaces to the module map.
  // This allows importing importing from all the project's packages.
  watchFolders: {
    '/Users/me/my-app/node_modules',
    '/Users/me/my-app/packages/app/',
    '/Users/me/my-app/packages/build-tools/',
    '/Users/me/my-app/packages/mobile/'
  },
  resolver: {
    // Ensure we resolve nohoist libraries from this directory.
    // With "((?!mobile).)", we're blocking all the cases were metro tries to
    // resolve nohoisted libraries from a directory that is not "mobile".
    blockList: exclusionList([
      /^((?!mobile).)*\/node_modules\/@react-native-community\/cli-platform-ios\/.*$/,
      /^((?!mobile).)*\/node_modules\/@react-native-community\/cli-platform-android\/.*$/,
      /^((?!mobile).)*\/node_modules\/hermes-engine\/.*$/,
      /^((?!mobile).)*\/node_modules\/jsc-android\/.*$/,
      /^((?!mobile).)*\/node_modules\/react\/.*$/,
      /^((?!mobile).)*\/node_modules\/react-native\/.*$/,
      /^((?!mobile).)*\/node_modules\/react-native-codegen\/.*$/,
    ]),
    extraNodeModules: {
      "@react-native-community/cli-platform-ios":
        "/Users/me/my-app/packages/mobile/node_modules/@react-native-community/cli-platform-ios",
      "@react-native-community/cli-platform-android":
        "/Users/me/my-app/packages/mobile/node_modules/@react-native-community/cli-platform-android",
      "hermes-engine":
        "/Users/me/my-app/packages/mobile/node_modules/hermes-engine",
      "jsc-android":
        "/Users/me/my-app/packages/mobile/node_modules/jsc-android",
      react: "/Users/me/my-app/packages/mobile/node_modules/react",
      "react-native":
        "/Users/me/my-app/packages/mobile/node_modules/react-native",
      "react-native-codegen":
        "/Users/me/my-app/packages/mobile/node_modules/react-native-codegen",
    },
  },
};
```

You should finally be able to to run your app on iOS now:

![](https://mmazzarolo.com/static/2ef8c3c2b7d3e2b2b6015eef23bc7dba/77672/ios.png)

## Fixing the Android assets resolution bug

If you run your app on Android, you'll notice that images won't be loaded correctly:

![](https://mmazzarolo.com/static/101c006738d891c77b9abb9e4524fda8/78958/android-ko.png)

This is because of [an open issue with the metro bundler logic used to load assets outside of the root directory on android](https://github.com/facebook/metro/issues/290) (like our `app/src/logo.png` image).

To fix this issue, we can patch the metro bundler assets resolution mechanism by adding a custom server middleware in the metro config.  
The way the fix works [is quite weird](https://github.com/facebook/metro/issues/290#issuecomment-543746458), but since it's available in `react-native-monorepo-tools` you shouldn't have to worry too much about it.

You can add it to metro the metro config this way:

```diff
 const path = require("path");
 const exclusionList = require("metro-config/src/defaults/exclusionList");
 const {
   getMetroConfig,
   getAndroidAssetsResolutionFix,
 } = require("react-native-monorepo-tools");

 const yarnWorkspacesMetroConfig = getMetroConfig();

+const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix();

 module.exports = {
   transformer: {
     getTransformOptions: async () => ({
+      // Apply the Android assets resolution fix to the public path...
+      publicPath: androidAssetsResolutionFix.publicPath,
+      transform: {
+        experimentalImportSupport: false,
+        inlineRequires: false,
+      },
+    }),
   },
+  server: {
+    // ...and to the server middleware.
+    enhanceMiddleware: (middleware) => {
+      return androidAssetsResolutionFix.applyMiddleware(middleware);
+    },
+  },
   // Add additional Yarn workspace package roots to the module map.
   // This allows importing importing from all the project's packages.
   watchFolders: yarnWorkspacesMetroConfig.watchFolders,
   resolver: {
     // Ensure we resolve nohoist libraries from this directory.
     blockList: exclusionList(yarnWorkspacesMetroConfig.blockList),
     extraNodeModules: yarnWorkspacesMetroConfig.extraNodeModules,
   },
 };
```

Try running Android — it should work correctly now 👍

![](https://mmazzarolo.com/static/4a339c974b8b553b6afcc315ecd24b40/78958/android-ok.png)

## Developing and updating the app

By using `react-native-monorepo-tools` in the metro bundler configuration, we are consolidating all our Yarn workspaces settings into the root `package.json`'s `nohoist` list.  
Whenever we need to add a new library that doesn't work well when hoisted (e.g., a native library), we can add it to the `nohoist` list and run `yarn` again so that the metro config can automatically pick up the updated settings.

Additionally, since we haven't touched the native code, updating to newer versions of React Native shouldn't be an issue (as long as there aren't breaking changes in metro bundler).

## Root-level scripts

To improve a bit the developer experience, I recommend adding a few scripts to the top-level `package.json` to invoke workspace-specific scripts (to avoid having to `cd` into a directory every time you need to run a script).

For example, you can add the following scripts to the mobile workspace:

```json
"scripts": {
  "android": "react-native run-android",
  "ios": "react-native run-ios",
  "start": "react-native start",
  "studio": "studio android",
  "xcode": "xed ios"
},
```

And then you can reference them from the root this way:

```json
"scripts": {
  "android:metro": "yarn workspace @my-app/mobile start",
  "android:start": "yarn workspace @my-app/mobile android",
  "android:studio": "yarn workspace @my-app/mobile studio",
  "ios:metro": "yarn workspace @my-app/mobile start",
  "ios:start": "yarn workspace @my-app/mobile ios",
  "ios:xcode": "yarn workspace @my-app/mobile xcode"
},
```

This pattern allows us to run workspace-specific script directly from the root directory.

## Next steps

In the next step, we'll add support for Windows and macOS to our monorepo.

Stay tuned!

- [Overview](https://dev.to/mmazzarolo/running-react-native-everywhere-4gpp)
- [Monorepo setup](https://dev.to/mmazzarolo/running-react-native-everywhere-yarn-workspaces-monorepo-3j5g)
- **Android & iOS (☜ you're here)**
- [Windows & macOS](https://dev.to/mmazzarolo/running-react-native-everywhere-windows-macos-7lp)
- [The Web](https://dev.to/mmazzarolo/running-react-native-everywhere-the-web-h6d)
- [Electron & browser extension](https://dev.to/mmazzarolo/running-react-native-everywhere-browser-extensions-electron-46hh)

> _Originally published at [mmazzarolo.com](https://mmazzarolo.com/blog/2021-09-18-running-react-native-everywhere-mobile/)_