How to create React Native App for Android, iOS and Web without Expo
Tags: react, reactnative, tutorial, javascript
Published: 2021-06-12 15:47:03

> **Note:** This article is updated on 08-Aug-2021, to solve [this StackOverflow issue](https://stackoverflow.com/questions/68014681/react-native-run-android-error-in-emulator-could-not-get-batchedbridge-make-su), In this update, Creation of `.babelrc` is removed, `babel.config.js` & `webpack.config.js` are updated. The changes are equivalent to [this commit](https://github.com/ShivamS136/react-native-web-setup/commit/c73b8c4139f0b86fc3d1734f1bc8a7014d00e24b).

---



In this article, we are going to see, how to create a React Native App which can run on Android, iOS and Web Browser. We'll NOT be using [Expo](https://expo.io/) in this process. For web support, we'll be using the package [react-native-web](https://github.com/necolas/react-native-web).

I am using a Windows machine so I'll be showing the project run in Android and Web only. I am assuming that you have already downloaded and setup Node, NPM, Android SDK, Java and Emulator/Device for build and debugging purpose. If not, don't worry [follow this article](https://dev.to/shivams136/how-to-install-android-sdk-without-android-studio-ff2).

**My Environment:**
* **OS:** Windows 10 (64 Bit)
* **Node:** 16.3.0
* **NPM:** 7.17 

---

In case, if you want to explore more about which Hybrid App Development Framework to choose then you can have a look at this article: [React Native vs Ionic vs Flutter](https://dev.to/shivams136/hybrid-app-development-comparison-among-react-native-ionic-and-flutter-1lb)

---

## Step 1: Init a React Native Application:
This step is same as the [official React Native Doc](https://reactnative.dev/docs/environment-setup#:~:text=Creating%20a%20new%20application). So to `init` a React Native app:
![Command-Prompt-Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dibxex97wzxh76d7gm3m.png)
- Open command Prompt and go to the path where you want to create the project, In my case the path is `C:\Users\shivam\Desktop\React`.
- Init App:
```sh
npx react-native init AwesomeProject
```
- Your folder will be looking like [this commit](https://github.com/ShivamS136/react-native-web-setup/commit/f66c8f85665ab1eb0bbef563b069762ad54baf5d).
- You'll find a new folder `AwesomeProject` in the current Directory, Now open this folder using any editor, I am using [Visual Studio Code](https://code.visualstudio.com/).

---

## Step 2: Run this new app in Android (optional)
If you have Android setup done and an emulator or device connected, then you can simply run the app in android by simply running below command using command prompt in the folder `AwesomeProject`.
```sh
npx react-native run-android
```

---

## Step 3: Web Setup
As already mentioned we'll be using  the package [react-native-web](https://github.com/necolas/react-native-web) for web support. So you can find short setup instructions in [the official doc of this package](https://necolas.github.io/react-native-web/docs/).

---

### 1. Add Web packages
Add the package [`react-native-web`](https://www.npmjs.com/package/react-native-web) for web APIs and [`react-dom`](https://www.npmjs.com/package/react-dom) for browser.
```sh
npm install react-dom react-native-web
```
If you see any error saying `unable to resolve dependency tree` then you can use the `--legacy-peer-deps` option like below.
```sh
npm install react-dom react-native-web --legacy-peer-deps
```

---

### 2. Babel Plugin for build time optimization
As per official doc's recommendation, use the [babel](https://babeljs.io/) plugin [`babel-plugin-react-native-web`](https://www.npmjs.com/package/babel-plugin-react-native-web).
```sh
npm install --save-dev babel-plugin-react-native-web
```

---

### 3. Babel Module Aliasing
As we are going to alias `react-native` as `react-native-web` so as [Babel](https://babeljs.io/) supports module aliasing using [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver), we'll be using this.
```sh
npm install --save-dev babel-plugin-module-resolver
```
Now, to set alias, we'll use this package in webpack configuration. *(These settings will be inside `web/webpack.config.js`, we'll introduce this file later in this article)*

---

### 4. Mock Jest
[Jest](https://facebook.github.io/jest/) can be configured using the provided preset. This will map `react-native` to `react-native-web` and provide appropriate mocks. 
For this, in `/package.json` file, update the value of the key `"jest"` from `"react-native"` to `"react-native-web"`. Final value:

**/package.json**
```json'
{
  // Other Settings
  "jest": {
    "preset": "react-native-web"
  }
}
```

---

### 5. Configure Flow
[Flow](https://flow.org/) is  a static type checker for Javascript like [TypeScript](https://www.typescriptlang.org/). It's used by React-Native by default if you don't init the project using TypeScript template. 

It can be configured to understand the aliased module. For this we need to add some config text under `[options]` key inside the file `/.flowconfig`.

**/.flowconfig**
```yml
[options]
# Alias the package name
module.name_mapper='^react-native$' -> 'react-native-web'
```
Flow can be configured to pull types from React Native for Web’s source code. For that add below config text also in the `[options]` key.

**/.flowconfig**
```yml
[options]
# Point flow to the 'module' field by default
module.system.node.main_field=module
module.system.node.main_field=main
```

---

### 6. Package Optimization
We added a package [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver) in step-3, It is recommended for build-time optimizations and to prune modules not used by your application. To config this we'll use webpack configurattions so your `/babel.config.js` file should look something like below.

**/babel.config.js**
```js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
};
```

---

### 7. Create Entry Files
For web version, we need to create 2 entry files, First one is `index.html` and second one is `index.web.js`, both needs to be placed at root path.

**/index.html**
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Testing 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**
```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 notice this almost same as `index.js` except the last line. As you can see above, we are using an App component *but from where did it came?* So it's the same `App.js` file copied with the name `App.jsx` inside the path `/src/components/`, It's just to demonstrate an important concept which we'll learn later in this article, as this file will be creating some problems.  So `/src/components/App.jsx` will look like below:

> **Note:** As in many Editors/IDE the same App.js file can show errors as the file is using Flow syntax for type definitions and this syntax might not be supported in your IDE, to solve this you can either add Flow Language support via extensions or remove the flow specific code. I have removed the flow specific code in the below example for saving your time in troubleshooting.

**/src/components/App.jsx** 
```jsx
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */
 
import React from 'react';
import {Node} from 'react';
import {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  View,
} from 'react-native';

import {
  Colors,
  DebugInstructions,
  Header,
  LearnMoreLinks,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

const Section = ({children, title}) => {
  const isDarkMode = useColorScheme() === 'dark';
  return (
    <View style={styles.sectionContainer}>
      <Text
        style={[
          styles.sectionTitle,
          {
            color: isDarkMode ? Colors.white : Colors.black,
          },
        ]}>
        {title}
      </Text>
      <Text
        style={[
          styles.sectionDescription,
          {
            color: isDarkMode ? Colors.light : Colors.dark,
          },
        ]}>
        {children}
      </Text>
    </View>
  );
};

const App = () => {
  const isDarkMode = useColorScheme() === 'dark';

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
      <ScrollView
        contentInsetAdjustmentBehavior="automatic"
        style={backgroundStyle}>
        <Header />
        <View
          style={{
            backgroundColor: isDarkMode ? Colors.black : Colors.white,
          }}>
          <Section title="Step One">
            Edit <Text style={styles.highlight}>App.js</Text> to change this
            screen and then come back to see your edits.
          </Section>
          <Section title="See Your Changes">
            <ReloadInstructions />
          </Section>
          <Section title="Debug">
            <DebugInstructions />
          </Section>
          <Section title="Learn More">
            Read the docs to discover what to do next:
          </Section>
          <LearnMoreLinks />
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
  },
  highlight: {
    fontWeight: '700',
  },
});

export default App;
```

---

### 8. Configuring and budling
We'll be using [Webpack](https://www.npmjs.com/package/webpack) for bundling and [Babel](https://babeljs.io/) for transpiling along with `babel-loader`.

**Install Webpack and related dependencies:** Run below command in the terminal to install packages for dev environment.
```sh
npm install --save-dev babel-loader url-loader webpack webpack-cli webpack-dev-server
```

**Tree-Shaking:** React Native’s Babel preset rewrites ES modules to CommonJS modules, preventing bundlers from automatically performing “tree-shaking” to remove unused modules from your web app build. To help with this, you can install the following Babel plugin:
```sh
npm install --save-dev babel-plugin-react-native-web
```

**Webpack Configuration:**
This cofiguration is picked from [the official doc](https://necolas.github.io/react-native-web/docs/multi-platform/) and slightly modified to add `.jsx` support and `module-resolver` we added above via `babel-plugin-module-resolver`. So to configure Webpack create a file at `/web/webpack.config.js`. We will be using [`webpack-cli`](https://www.npmjs.com/package/webpack-cli) to diffrentiate between development and production builds, if you want to manage this via script then you can use [this guide](https://webpack.js.org/guides/production/).

**/web/webpack.config.js**
```js
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'],
  },
};
```

### 9. Scripts to run on web
Now we are going to add some scripts to run our web app with short command instead of full webpack-cli command. For this, we need to add below two options in the file `/package.json` inside `"scripts"` key.

To know more about `webpack-cli` options for webpack-5 go [here](https://github.com/webpack/webpack-cli/blob/master/OPTIONS.md) and to know more about dev-tool go [here](https://webpack.js.org/configuration/devtool/)

**/package.json**
```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"
  }
}
```

---

### 10. RUN OUR WEBAPP
So, Finally we are here, as we have set shortcut in our `package.json` for script so now we can simply run below command to start our webapp in browser.
```sh
npm run web
```

***Wait a minute!!!*** I'm getting error, like below:
```sh
ERROR in ./node_modules/react-native/Libraries/NewAppScreen/components/DebugInstructions.js 11:12
Module parse failed: Unexpected token (11:12)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|  */
|
> import type {Node} from 'react';
| import {Platform, StyleSheet, Text} from 'react-native';
| import React from 'react';
 @ ./node_modules/react-native/Libraries/NewAppScreen/index.js 17:0-63 20:0-27:2
 @ ./src/components/App.jsx 1:864-910
 @ ./index.web.js 1:261-292
 ```
 
So this is the error we talked about in **Step 7**. I literally spent 3-4 days to find a solution for this. And then the creator and maintainer of `react-native-web` package, [Nicolas Gallagher](https://github.com/necolas) helped me via [this discussion](https://github.com/necolas/react-native-web/discussions/2052). 
So the issue is in the `import` statement on line 21 of `src/components/App.jsx`, where we are trying to do something like below:

> This line in the stack trace is telling us that we're trying to bundle RN internal code in our web bundle: `node_modules/react-native/Libraries/NewAppScreen/index.js`.
>
> 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. 


> ```js
> import {
>  Colors,
>  DebugInstructions,
>  Header,
>  LearnMoreLinks,
>  ReloadInstructions,
> } from 'react-native/Libraries/NewAppScreen';
> ```
> 
> All this code of the problem. We should not import from Libraries. This is not part of the public API.

To solve this, remove the dependency on the library: `react-native/Libraries`, for that update the code of `/src/components/App.jsx` as below:

**/src/components/App.jsx**
```jsx
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

import React from 'react';
import {Node} from 'react';
import {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  View,
} from 'react-native';

// import {
//   Colors,
//   DebugInstructions,
//   Header,
//   LearnMoreLinks,
//   ReloadInstructions,
// } from 'react-native/Libraries/NewAppScreen';

const Colors = {
  white: '#fff',
  black: '#000',
  light: '#ddd',
  dark: '#333',
  lighter: '#eee',
  darker: '#111',
};

const Section = ({children, title}) => {
  const isDarkMode = useColorScheme() === 'dark';
  return (
    <View style={styles.sectionContainer}>
      <Text
        style={[
          styles.sectionTitle,
          {
            color: isDarkMode ? Colors.white : Colors.black,
          },
        ]}>
        {title}
      </Text>
      <Text
        style={[
          styles.sectionDescription,
          {
            color: isDarkMode ? Colors.light : Colors.dark,
          },
        ]}>
        {children}
      </Text>
    </View>
  );
};

const App = () => {
  const isDarkMode = useColorScheme() === 'dark';

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
      <ScrollView
        contentInsetAdjustmentBehavior="automatic"
        style={backgroundStyle}>
        {/* <Header /> */}
        <View
          style={{
            backgroundColor: isDarkMode ? Colors.black : Colors.white,
          }}>
          <Section title="Step One">
            Edit <Text style={styles.highlight}>App.js</Text> to change this
            screen and then come back to see your edits.
          </Section>
          <Section title="See Your Changes">
            {/* <ReloadInstructions /> */}
            <Text>Reload Instruction</Text>
          </Section>
          <Section title="Debug">
            {/* <DebugInstructions /> */}
            <Text>Debug Instruction</Text>
          </Section>
          <Section title="Learn More">
            Read the docs to discover what to do next:
          </Section>
          {/* <LearnMoreLinks /> */}
          <Text>Learn More Links</Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
  },
  highlight: {
    fontWeight: '700',
  },
});

export default App;
```

---

### 11. Finally run after Troubleshooting
We can simply run below command in the terminal if it was stopped previously to start our webapp in browser.
```sh
npm run web
```
You should get output similar to below screenshot, and you can go to [http://localhost:8080](http://localhost:8080) to see your webapp running.

![Final Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nqdff7rm3hyrylbbdt9p.png)

---

I'm sure it will help someone, at least it could save my 4 days if I know this before. All the changes done to add web support can be found in [this before-update commit](https://github.com/ShivamS136/react-native-web-setup/commit/d732e1dab375024dad770454dab68b500a9bd78d) and [this after-update commit](https://github.com/ShivamS136/react-native-web-setup/commit/c73b8c4139f0b86fc3d1734f1bc8a7014d00e24b). 

I created [a release](https://github.com/ShivamS136/react-native-web-setup/releases/tag/1.0.0) also for the same. 

I used the same configurations on [this Dummy project](https://github.com/ShivamS136/harry-potter-dictionary) and faced no issues after update.

If you find any issue in the configurations and gets it resolved, don't hesitate to contribute to [the repo](https://github.com/ShivamS136/react-native-web-setup).

You can connect with me on [Linkedin](https://www.linkedin.com/in/shivams136/) or [Github](https://github.com/ShivamS136/) as well.