React Native for Web Apps
Tags: reactnative, react, javascript
Published: 2022-10-26 15:03:09

Wouldn't it be cool if we could just write an app once? In stead 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.

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 web, using the same code we use to deploy the Native version for Google Play or App Store. 

That package is [react-native-web](https://www.npmjs.com/package/react-native-web)

And you can also learn a lot more about it [here](https://necolas.github.io/react-native-web/)

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 MVP's.

Let's take a look into more detail on how all this is done, step by step:

You will fist need several dependencies to run the React Native CLI. (Node, watchman, cocoapods, react-native)
And also x-code which is super heavy and takes a bit to download form [the official apple link](https://developer.apple.com/xcode/).

Make sure Node version is 14 or above.

You can get more details on alternative setups [here](https://reactnative.dev/docs/environment-setup).

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 web. 

**As any react project**, node package execute:

`npx react-native init MyAwesomeProject`

**As Expo** project:

`npm install --global expo-cli && npx create-expo-app MyAwesomeProject`

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 `view`s got compiled into `html`.

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

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.

```javascript
/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 via [this discussion](https://github.com/necolas/react-native-web/discussions/2052).

> "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 back in track for our export. 

Next, we'll need 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](https://necolas.github.io/react-native-web/docs/multi-platform/) with a small twist to add jsx support ;).

`webpack.config.js`:

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

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

// This is needed for webpack to compile JavaScript.
// Many OSS React Native packages are not compiled to ES5 before being
// published. If you depend on uncompiled packages they may cause webpack build
// errors. To fix this webpack can be configured to compile to the necessary
// `node_module`.
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 setup 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 get's 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](https://necolas.github.io/react-native-web)
[Multi-Platform-React-Native](https://necolas.github.io/react-native-web/docs/multi-platform/)
[Setup - react-native-web](https://necolas.github.io/react-native-web/docs/setup/)
[React Native for Web without Expo - by shivams136](https://dev.to/shivams136/create-react-native-app-for-android-ios-and-web-without-expo-48lc)