TL;DR
Fourth part of the "React Native monorepo for every platform" 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";
// 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
:
{
"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"
]
}
}
{
"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
npx create-react-app my-app && mv my-app web
Rename the package name:
{
- "name": "my-app",
+ "name": "@my-app/web",
"version": "0.0.0",
"private": true,
{
- "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
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:
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")
);
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
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 theweb
workspace. - Use Webpack's aliases to ensure all the libraries in the
nohoist
list are resolved fromweb/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 (likeprocess.env.NODE_ENV
on the web).
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",
}),
],
},
};
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:
{
"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"
},
{
"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:
"scripts": {
"web:start": "yarn workspace @my-app/web start",
"web:build": "yarn workspace @my-app/web build"
},
"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.
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:
- Using the
platform
module. - Using platform-specific file extensions.
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!
- Overview
- Monorepo setup
- Android & iOS
- Windows & macOS
- The web (☜ you're here)
- Browser extensions & Electron