React Native for Web Apps

Foto del autor

By adm_akb

November 14, 2022

By Raul Sposito, Software Developer @ Arionkoder.

Wouldn’t it be cool if we could just write an app once? Instead of having to create a native version for mobile and then develop a whole new codebase for a Web App? Wouldn’t it be much more time/cost-effective?

React Native is a full JavaScript framework and one of the cool things it does is that it translates the React Native tags into Native tags, to Kotlin/Java in the case of Android and to Swift/ObjC in the case of IOS.

Write once, run anywhere!

So in the case of adding a View tag, it would translate to ViewGroup in Kotlin or UIView in the case of Swift. And we don’t have to worry about all that, RN just does it for us.

So following this logic, we should just have a package that translates the native modules into HTML elements compiled on the browser, and that way we can just create the build for the web, using the same code we use to deploy the Native version for Google Play or App Store.

That package is react-native-web

And you can also learn a lot more about it here.

If you’re thinking this is crazy or kind of a long shot, let me tell you several names in the industry (big names) use this same package to build their web apps from their actual React Native codebase.

With this package we can create a new build for the web that we can deploy to AWS amplify or Firebase hosting or whatever host you like. It would be a full web build you can deploy. 🙂

So we’re covered! We have IOS, Android, and Desktop and we only had to code it once.

Unless you’re making something super intricate or specific in your app, this should cover most simple projects or MVPs.

Let’s take a look in more detail at how all this is done, step by step:

You will first need several dependencies to run the React Native CLI. (Node, watchman, cocoa pods, react-native)
And also x-code which is super heavy and takes a bit to download from the official apple link.

Make sure the Node version is 14 or above.

You can get more details on alternative setups here.

Then, you can choose to start the project with just React Native or we can use Expo which would simplify the build process for us whenever we are exporting for the web.

As with any react project, node package execute:

`npx react-native init my-awesome-project`

As Expo project:

`npm install — global expo-cli && npx create-expo-app my-awesome-project`

As you would expect this will create the basic template and folder structure for you to start working on your project.

Then to start the project `npx react-native start` or `npx expo start`

This will start the metro bundler and pull up our project on the emulator.

Ok, now let’s make it web!

We need to add the dependencies `npm install react-dom react-native-web`

Cool, so after the installation we can pull up the project and we should see it in both mobile and web versions if we decide to go to the browser and type the localhost port, we should see the web version of our app.

In that web version, you might want to go ahead and inspect the page just to find out that it’s no longer React Native elements, they’re actually html in the browser now. The views got compiled into html.

Alright, how do we extract all this into a build so we can host it on our server?

The easiest way I know would be with Expo:

`expo build:web`

This will generate the new build in a root-level folder and that content we can host wherever we like on the web!

For the traditional version we should follow the docs advice and install a few plugins for optimization first:

`npm install — save-dev babel-plugin-react-native-web`

This Babel plugin is recommended for the build.

Next, we would need to alias our react-native into react-native-web so babel-plugin-module-resolver is required.

`npm install — save-dev babel-plugin-module-resolver`

This will all be configured from our webpack config file, so go ahead and add the following to your babel.config.js file:

`npm install — save-dev babel-plugin-module-resolver`

All good so far, now for our web version we will need to create two entry files living at root path. index.html and index.web.js.

/index.html

html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”utf-8" />
<title>React Native on the Web!</title>
<meta content=”initial-scale=1,width=device-width” name=”viewport” />
<meta httpEquiv=”X-UA-Compatible” content=”IE=edge” />
<style>
/* These styles make the body full-height */
html,
body,
#root {
height: 100%;
}
/* These styles disable body scrolling if you are using <ScrollView> */
body {
overflow: hidden;
}
/* These styles make the root element flex and column wise filling */
#root {
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div id=”react-native-web-app”></div>
<script type=”text/javascript” src=”/bundle.web.js”></script>
</body>
</html>

Notice the script name src=”/bundle.web.js”, We’ll be using this file name while configuring webpack.

/index.web.js
import React from 'react';
import {AppRegistry} from 'react-native';
import App from './src/components/App';
import {name as appName} from './app.json';

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

AppRegistry.runApplication(appName, {
rootTag: document.getElementById('react-native-web-app'),
});

If you look at the file above, you might notice that on the third line `import App from ‘./src/components/App’;` I’m importing an App component, which is different from our standard output for our React Native App. This is because, we will need a differential output for this build to happen, otherwise we would get an error because of the imports we are using that are not part of the RN public API.

For example:

javascript
import {
Colors,
Header,
ReloadInstructions,
} from ‘react-native/Libraries/NewAppScreen’;

As stated by Nicolas Gallagher in this discussion.

> “First, we should not be loading any of the RN package on web, especially not parts that aren’t part of the public API . Second, as mentioned in the inline comments of the config web/webpack.config.js, we need to explicitly list everything in node_modules that needs compiling.”

So this is basically why we create a different file, to reference as output. And that file would not have the imports or would not be referencing any of the library components we used for our Mobile version. This will have to be resolved in a ‘web’ way for this one.

Great! We got ahead of that issue and are back on track for our export.

Next, we’ll need to dive into Compiling and Bundling using Webpack for bundling and Babel for transpiling along with babel-loader.

`npm install — save-dev babel-loader url-loader webpack webpack-cli webpack-dev-server`

`npm install — save-dev babel-plugin-react-native-web`

Cool, now let’s create a `/web/webpack.config.js` file and start configuring webpack using the official docs with a small twist to add jsx support ;).

`webpack.config.js`:

const path = require('path');
const webpack = require('webpack');

const appDirectory = path.resolve(__dirname, '../');

const babelLoaderConfiguration = {
test: /\.(js)|(jsx)$/,
// Add every directory that needs to be compiled by Babel during the build.
include: [
path.resolve(appDirectory, 'index.web.js'),
path.resolve(appDirectory, 'src'),
path.resolve(appDirectory, 'node_modules/react-native-uncompiled'),
],
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// The 'metro-react-native-babel-preset' preset is recommended to match React Native's packager
presets: ['module:metro-react-native-babel-preset'],
// Re-write paths to import only the modules needed by the app
plugins: [
'react-native-web',
[
'module-resolver',
{
alias: {
'^react-native$': 'react-native-web',
},
},
],
],
},
},
};

// This is needed for webpack to import static images in JavaScript files.
const imageLoaderConfiguration = {
test: /\.(gif|jpe?g|png|svg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
esModule: false,
},
},
};

module.exports = {
entry: [
// load any web API polyfills
// path.resolve(appDirectory, 'polyfills-web.js'),
// your web-specific entry file
path.resolve(appDirectory, 'index.web.js'),
],

// configures where the build ends up
output: {
filename: 'bundle.web.js',
path: path.resolve(appDirectory, 'dist'),
},

// ...the rest of your config

module: {
rules: [babelLoaderConfiguration, imageLoaderConfiguration],
},

resolve: {
// This will only alias the exact import "react-native"
alias: {
'react-native$': 'react-native-web',
},
// If you're working on a multi-platform React Native app, web-specific
// module implementations should be written in files using the extension
// `.web.js`.
extensions: ['.web.js', '.js', '.jsx'],
},
};

We are ALMOST there!

Just need to set up which scripts we’ll be using to actually run this on the web. So let’s take a look at `/package.json`:

{
"scripts": {
"web": "webpack serve -d source-map --mode development --config \"./web/webpack.config.js\" --inline --color --hot",
"build:web": "webpack --mode production --config \"./web/webpack.config.js\" --hot"
}
}

After adding the correct scripts we should be able to run npm run web on the command line and get our app to run!

You might want to navigate to port http://localhost:8080 to see your webapp running.

**Conclusion:**
As you might have noticed, the Expo path is waaaay simpler and gets a lot done for you. If you want to dive into the regular RN way, I really hope this guide serves a purpose for that matter.

I would recommend using the tools available to make it simple, as stated on the official docs:

> “If you are interested in making a multi-platform app it is strongly recommended that you use Expo (or learn from the source code for the Web integration). Expo includes web support and takes care of all the configuration work required.”

Feel free to build for ANY platform!

Hope you enjoyed the extensive article 🙂

react-native-web docs
Multi-Platform-React-Native
Setup — react-native-web
React Native for Web without Expo — by shivams136