Running React Native everywhere: The Web

Sep 21, 2021

TL;DR

Fourth part of the “Running React Native everywhere” series: a tutorial about structuring your monorepo to run multiple React Native apps targeting different platforms.

This time, we’ll focus on running React Native on the web.

About React Native for Web

React Native for Web is an accessible implementation of React Native’s components and APIs that is interoperable with React DOM.
React Native for Web translates all the references of React Native’s components (e.g. View) and APIs to their HTML & DOM counterpart (e.g., div).

The React Native for Web homepage does a great job at highlighting why you should try it:

  • Accessible HTML. Support different devices and input modes, render semantic tags.
  • High-quality interactions. Support gestures and multiple input modes (touch, mouse, keyboard).
  • Reliable styles. Rely on scoped styles and automatic vendor-prefixing. Support RTL layouts.
  • Responsive containers. Respond to element resize events.
  • Incremental adoption. Interoperates with existing React DOM components. Bundle only what you use.

If you’ve already built a React Native app and you’re planning to port it to the web, I recommend giving React Native for Web a try.

One of the most common mistakes people make about React Native for Web is assuming that it is React Native.
Let’s be clear: in a React Native for Web project you’re not “using” React Native, you’re just aliasing every component and API from react-native to react-native-web.

// Running the following:
import { Button, Image, StyleSheet, Text, View } from "react-native";
// At build time is translated to:
import { Button, Image, StyleSheet, Text, View } from "react-native-web";

Instead of thinking about React Native for Web as a library for building mobile apps that run on the web, think of it as a website that uses React Native as a “components and API framework”.

Because React Native for Web is a React website, you can use front-end tools to build and run it.
For example, you can build it with Webpack or Rollup instead of Metro bundler.

Like for React Native for Windows + macOS, you can add React Native for Web to an existing mobile project. I’ve already written about this option in the past in “Run your React Native app on the web with React Native for Web”.
However, in this tutorial we’ll add it as a separate web workspace.

Create React App (CRA)

React Native for Web is compatible with multiple frameworks and tools. You can use it with Create React App, Next.js, Gatsby, Expo (!), or you can create a custom build process.

To keep it simple, in this tutorial we’ll use Create React App, which is a basic way to setup a simple, web-only React app with built-in support for aliasing react-native-web to react-native.

Create React App is very limited in its configuration options, so we’ll use CRACO (Create React App Configuration Override) to customize its Webpack configuration to make it compatible with Yarn workspaces.

Setup

First of all, add the react-native-web library to the nohoist list in the root’s package.json:

my-app/package.json
 {
   "name": "my-app",
   "version": "0.0.1",
   "private": true,
   "workspaces": {
     "packages": [
       "packages/*"
     ],
     "nohoist": [
       "**/react",
       "**/react-dom",
       "**/react-native",
       "**/react-native/**",
       "**/react-native-windows",
+      "**/react-native-web"
     ]
   }
 }

Then, from the packages directory, scaffold a new Create React App project:

npx create-react-app my-app && mv my-app web

Rename the package name:

my-app/packages/web/package.json
 {
-  "name": "my-app",
+  "name": "@my-app/web",
   "version": "0.0.0",
   "private": true,

And install react-native-web:

cd web && yarn add react-native-web

The cool thing about Create React App is that adding react-native-web to our dependencies is enough to make it automatically resolve react-native-web instead of react-native.

To start using our React Native app within the web project, add it to the JavaScript entry point:

my-app/packages/web/src/index.js
 import React from "react";
 import ReactDOM from "react-dom";
 import "./index.css";
-import App from "./App";
+import { App } from "@my-app/app";

 ReactDOM.render(
   <React.StrictMode>
     <App />
   </React.StrictMode>,
   document.getElementById("root")
 );

If Create React App supported Yarn workspaces out-of-the-box, what we’ve done so far would have been enough to run the app… unfortunately, it doesn’t.
Luckily, we can use CRACO (or other tools such as customize-cra or react-app-rewired) to customize the Webpack configuration used by Create React App to resolve packages imported from other workspaces.

Install CRACO and react-native-monorepo-tools:

yarn add -D @craco/craco react-native-monorepo-tools

Create a craco.config.js file at the root of your web workspace.
We’ll use it to:

  • Update the babel-loader config used in Webpack to allow importing from directories outside of the web workspace.
  • Use Webpack’s aliases to ensure all the libraries in the nohoist list are resolved from web/node_modules. This ensures the build process doesn’t bundle the same library twice if its present in multiple workspaces.
  • Inject the __DEV__ global variable in the codebase. __DEV__ is commonly used in React Native apps to determine if we’re running in development or production mode (like process.env.NODE_ENV on the web).
my-app/packages/web/craco.config.js
const webpack = require("webpack");
const { getWebpackTools } = require("react-native-monorepo-tools");

const monorepoWebpackTools = getWebpackTools();

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      // Allow importing from external workspaces.
      monorepoWebpackTools.enableWorkspacesResolution(webpackConfig);
      // Ensure nohoisted libraries are resolved from this workspace.
      monorepoWebpackTools.addNohoistAliases(webpackConfig);
      return webpackConfig;
    },
    plugins: [
      // Inject the React Native "__DEV__" global variable.
      new webpack.DefinePlugin({
        __DEV__: process.env.NODE_ENV !== "production",
      }),
    ],
  },
};

To use the updated Webpack configuration, swap react-scripts in favour of craco in the workspace start and build scripts:

my-app/packages/web/package.json
 {
   "name": "@my-app/web",
   "version": "0.0.0",
   "private": true,
   "scripts": {
-    "start": "react-scripts start",
+    "start": "craco start",
-    "build": "react-scripts build",
+    "build": "craco build",
     "test": "react-scripts test --watchAll=false --passWithNoTests",
     "eject": "react-scripts eject"
   },

And while you’re at it, update the root package.json so that you can invoke the web scripts from the root of the monorepo:

my-app/package.json
"scripts": {
  "web:start": "yarn workspace @my-app/web start",
  "web:build": "yarn workspace @my-app/web build"
},

We’re done.
We can now run yarn web:start to run your app in development mode, and yarn web:build to create a production build.

screenshot

Compatibility and platform-specific code

React Native for Web provides compatibility with the vast majority of React Native’s JavaScript API. Features deprecated in React Native should be considered unsupported in React Native for Web.
See “React Native compatibility” for details.

Also, like for Windows and macOS, React Native provides two ways to organize your web-specific code and separate it from the other platforms:

On Expo & Next.js

In this tutorial we’re not using Expo because it’s not compatible (yet) with every platform we’re supporting. Still, Expo for Web supports React Native for Web out-of-the-box, provides dozens of additional cross-platform APIs, includes web build optimizations, and is compatible with the broader React Native ecosystem.

And thanks to @expo/next-adapter, you can even use Next.js to control your Expo for Web app.
For details, check “Using Next.js with Expo for Web”.

Next steps

In the next step, we’ll re-use the web codebase we just created as a boilerplate to support Electron and browser extensions.

Stay tuned!