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, I am keeping it updated 🙌
Why am I writing this?
Hi 👋 I'm Bruno Lemos. I recently launched a project called DevHub - TweetDeck for GitHub and one of the things that caught people's attention 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, it's a lib by 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 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.
Summary
- Starting a new
React Nativeproject - Turning our folder structure into a monorepo
- Making
react-nativework in a monorepo - Sharing code between our monorepo packages
- Creating a new web project using
create-react-appandreact-native-web - Making
CRAwork inside ourmonorepowith 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!
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, Yarn and React Native.
- 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 thepackages/mobilefolder - Edit the
namefield onpackages/mobile/package.jsonfrompackagenametomobile - Create this
package.jsonat the root directory to enableYarn Workspaces:
{
"name": "myprojectname",
"private": true,
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": []
}
"dependencies": {
"react-native": "0.61.3"
}
}
- Create a
.gitignoreat the root directory:
.DS_Store
.vscode
node_modules/
yarn-error.log
$ yarn
Making react-native work in a monorepo
Check where
react-nativegot 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 ofnodeandyarn. Also make sure to use the exact same version of dependencies between the monorepo packages, e.g."react": "16.11.0"on bothmobileandcomponents, not a different version between them.Open your favorite editor and use the
Search & Replacefeature to replace all occurrences ofnode_modules/react-native/with../../node_modules/react-native/.For react-native <= 0.59, open
packages/mobile/package.json. Yourstartscript currently ends in/cli.js start. Append this to the end:--projectRoot ../../.Open
packages./mobile/metro.config.jsand set theprojectRootfield on it as well so it looks like this:
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-nativedependency to the rootpackage.jsonto be able to bundle the JS:
"dependencies": {
"react-native": "0.61.3"
},
iOS changes
$ open packages/mobile/ios/myprojectname.xcodeproj/- Open
AppDelegate.m, findjsBundleURLForBundleRoot:@"index"and replaceindexwithpackages/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:
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.
Android changes
$ studio packages/mobile/android/- Open
packages/mobile/android/app/build.gradle. Search for the textproject.ext.react = [...]. Edit it so it looks like this:
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 thegetJSMainModuleNamemethod. Replaceindexwithpackages/mobile/index, so it looks like this:
@Override
protected String getJSMainModuleName() {
return "packages/mobile/index";
}
If you get the
Cannot get property 'packageName' on null objecterror, try disabling auto linking
You can now run the Android app! 💙 Press the "Run" green triangle button inside Android Studio and choose the emulator or device.
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.jsonwith the following contents:
{
"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,mobileis only using thecomponentspackage:
"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. Replaceimport App from './App'withimport 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, replaceWelcome to React Native!withWelcome 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.jsor.native.js. See example.
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 insidepackages/web/src) - Make sure the dependencies inside
package.jsonare the exact same between all monorepo packages. For example, update the "react" version to "16.9.0" (or any other version) on bothwebandmobilepackages. $ yarn add react-native-web react-art$ yarn add --dev babel-plugin-react-native-web- Create the file
packages/web/src/index.jswith the following contents:
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-nativeinside acreate-react-appproject, itswebpackconfig automatically alias it toreact-native-webfor us.
- Create the file
packages/web/public/index.csswith the following contents:
html,
body,
#root,
#root > div {
width: 100%;
height: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
- Edit
packages/web/public/index.htmlto include our CSS before closing theheadtag:
...
<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
.envfile (packages/web/.env) with the following content:
SKIP_PREFLIGHT_CHECK=true
$ yarn add --dev react-app-rewired- Replace the scripts inside
packages/web/package.jsonwith this:
"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.jsfile with the following contents:
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'swebpackconfig 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! 🎉
Some gotchas
-
react-native-websupports most of thereact-nativeAPI, but a few pieces are missing likeAlert,Modal,RefreshControlandWebView; - If you come across a dependency that doesn't work well with the monorepo structure, you may add it to the 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 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
corepackage that only contain logic and helper functions (no UI-related code); - For Next.js, you can check their official example with react-native-web
- For native windows, you can try 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);
- To install new dependencies, use the command
yarn workspace components add xxxfrom the root directory. To run a script from a package, runyarn workspace web start, for example; To run a script from all packages, runyarn workspaces run scriptname;
Thanks for reading! 💙
If you like react, consider following me here on Dev.to and on Twitter.
Links
- Source code: react-native-web-monorepo
- DevHub: devhubapp/devhub (production app using this structure + Desktop + TypeScript)
- Twitter: @brunolemos



Oldest comments (158)
Thanks, Bruno! I really needed this. Can this be applied to Lerna instead of Yarn Workspaces?
I've never used Lerna because Yarn Workspaces does all I need, so I don't know the answer. But let us know! 🙌
Can we achieve this by using npm?
No, Workspaces is a feature available on Yarn but not on the npm cli, afaik.
Nice article, thanks!
Any more info on „If you use expo, there may be some news coming to you soon“? 😇
I believe they are about to release built-in web support soon, but I have no insider information, it's just an impression I had after seeing some tweets from their team members. Also some of their dependencies recently got web support merged. You can ask them for more details: twitter.com/Baconbrix/status/10983...
Awesome, I think you might be on to something here 🧐
He's right 👉 expoweb.netlify.com/
Nice, thanks for the mention in the article!
Were you able to share code between web and mobile using nohoist? I've had problems with this approach in the past but I don't remember exactly why. It has some downsides but it's definitely much simpler to setup. Everything working well for you this way?
No, sry for taking so long to reply.
Our yarn workspace is just set up to include app / backend / website
And they do not share components.
The problem is that metro does not support symbol links. Which is what yarn workspace uses.
github.com/facebook/metro/issues/1
This is fantastic! Now let's see it with Next.js!
Next.js has an official example using react-native-web, check it out: github.com/zeit/next.js/tree/maste...
It's possible to have native apps instead of Electron, with the same code. We can use react-native-windows and react-native-macos, for example. In a few days I'll experiment with Marzipan, a secret official Apple project to run iOS apps on the mac.
throws error error: unknown option `--projectRoot'
On modifying
--projectRootto--projectRootsI get following errorLoading dependency graph...jest-haste-map: Watchman crawl failed. Retrying once with node crawler.You mean when trying to use jest? If not please check the repository to see what you made differently.
Sorry, I modified my comment. I am getting an error with --projectRoot. I was trying on my own repo. I'll be cloning your repo and then let you know if the problem persists.
I'm using the unreleased react-native 0.59 in this tutorial, some dependencies got a major bump so maybe there are some differences if you are using an older version.
did you find an alternative for --projectRoot please ?
Thanks Bruno , Can we use react-native run-ios or android at packages/mobile folder ? I want to start the simulator .
Yes you can, check the readme: github.com/brunolemos/react-native...
Thanks
Nice article, I have a bit similar example for code sharing between react-native and next.js with react-native-web here: infinidraw.live/
Source: github.com/msand/InfiniDraw
Expo version: github.com/msand/InfiniDrawExpo
But, it's not really clean architecturally, it's a single project rather than separate folders & package.json for web and native, causing some tradeoffs. Been thinking should slit it up a bit similar to this.