Let’s make our react-native
app work in the browser, the right way.
The code from this tutorial is available on GitHub:
You can fork it and use to start new projects with code sharing 🎉
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 , 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 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 CRA 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 --version react-native@next
$ 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. If you use expo, there may be some news coming to you soon. 👀
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/mobile
folder - Edit the
name
field onpackages/mobile/package.json
frompackagename
tomobile
- Create this
package.json
at the root directory to enableYarn Workspaces
:
- Create a
.gitignore
at the root directory:
$ yarn
Making react-native work in a monorepo
- Open your favorite editor, use the
Search & Replace
feature (usuallyCmd+Shift+H
) and replace all occurrences ofnode_modules/react-native/
with../../node_modules/react-native/
. Most files will be inside theios
andandroid
folders. - Open
packages/mobile/package.json
. Yourstart
script currently ends in/cli.js start
. Append this to the end:--projectRoot ../../
.
iOS changes
$ open packages/mobile/ios/myprojectname.xcodeproj/
- Open
AppDelegate.m
, findjsBundleURLForBundleRoot:@"index"
and replaceindex
withpackages/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! 💙
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: "../../../../"
]
- Open
packages/mobile/android/app/src/main/java/com/myprojectname/MainApplication.java
. Search for thegetJSMainModuleName
method. Replaceindex
withpackages/mobile/index
, so it looks like this:
@Override
protected String getJSMainModuleName() {
return "packages/mobile/index";
}
- Android Studio will show a Sync Now popup. Click on it.
You can now run the Android app! 💙
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:
-
[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 thecomponents
package:
- 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.js
or.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
) $ 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:
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 acreate-react-app
project, itswebpack
config automatically for us.
- Create the file
packages/web/public/index.css
with the following contents:
html,
body,
#root,
#root > div {
width: ;
height: ;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
- Edit
packages/web/public/index.html
to include our CSS before closing thehead
tag:
...
<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:
$ yarn add --dev react-app-rewired
- Replace the scripts inside
packages/web/package.json
with 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.js
file 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[].include = appIncludes
config.module.rules[] = null
config.module.rules[].oneOf[].include = appIncludes
config.module.rules[].oneOf[].options.plugins = [
require.resolve('babel-plugin-react-native-web'),
].concat(config.module.rules[].oneOf[].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
‘swebpack
config so it includes our monorepo packages in CRA’s build step
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-web
supports most of thereact-native
API, but a few pieces are missing likeAlert
,Modal
,RefreshControl
andWebView
; -
react-native link
may not work well with monorepo projects; to workaround this, instead of only install them usingyarn workspace mobile add xxx
, install them in the root directory as well:yarn add xxx -W
. Now you can link it and then later remove it from the rootpackage.json
.
Some tips
- 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); - To install new dependencies, use the command
yarn workspace components add xxx
from 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! 💙
Links
This content was originally published here.