Running React Native everywhere: Browser Extensions & Electron
Tags: javascript, webdev, react, reactnative
Published: 2021-09-28 14:07:52

---
title: Running React Native everywhere: Browser Extensions & Electron
published: true
date: 2021-09-28 14:07:00 UTC
tags: #javascript #webdev #react #reactnative
canonical_url: https://mmazzarolo.com/blog/2021-09-25-running-react-native-everywhere-electron-browser-ext/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5hcxlozbw4t5f51noszn.png
---

## TL;DR

Fourth part of the ["Running React Native everywhere"](https://dev.to/mmazzarolo/running-react-native-everywhere-4gpp) series: a tutorial about structuring your monorepo to run multiple React Native apps targeting different platforms.

This time, we'll focus on running React Native in an Electron app and in a browser extension.

## About web-based platforms

> **⚠️ This post is more of a fun experiment than a real tutorial :)**  
> I'm not aware of many React Native for Web apps running in Electron in production (besides [Ordinary Puzzles](https://ordinarypuzzles.com/) and [DevHub](https://devhubapp.com/)). And I've never heard of anyone running React Native for Web in a browser extension before.

Now that [we added support for React Native on the web](https://dev.to/mmazzarolo/running-react-native-everywhere-the-web-h6d/), we can leverage web-based frameworks to run our web app on different platforms:

- With [Electron](https://www.electronjs.org/), we can build cross-platform desktop apps to run our React Native for Web app.
- With the [WebExtension API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) (for Firefox) and the Chrome [Extension API](https://developer.chrome.com/extensions) (for Chrome, Edge, Opera, and Vivaldi), we can run our React Native for Web app in a browser extension.

In both cases, we'll re-use our web app workspace as the foundation.

If you’re not familiar with web development this section will feel somewhat different from the rest of the tutorial because we won't work with anything really specific to React Native.  
This is more about adding support for Electron and a browser extension to a web app. Still, I think it's still a valuable example of **how our React Native JavaScript code can run everywhere**.

## Electron

[Electron](https://www.electronjs.org/) is a popular framework for building cross-platform desktop apps with JavaScript, HTML, and CSS.  
Many popular apps like Visual Studio Code or Slack are built with Electron.

Let's start by addressing the elephant in the room: yes, Electron apps can (and often do) perform poorly and not fit in with the rest of the operative system. That said, Electron is still a valid option for shipping desktop apps on platforms not yet supported by React Native (e.g., Linux) or if you don't want to (or can't) deal with Windows/macOS native code.

> This tutorial will show you the bare minimum setup required to develop your React Native for Web app on Electron.  
> If you're interested in a more in-depth tutorial, please check ["Building a desktop application using Electron and Create React App"](https://mmazzarolo.com/blog/2021-08-12-building-an-electron-application-using-create-react-app/).

Let's start by duplicating the React Native for Web workspace into a new `electron` one.

From the `packages/` directory, run:

```bash
cp -R web electron && cd electron
```

Add the following dependencies (most of them are here only to simplify the development flow):

```bash
yarn add -D concurrently cross-env electron electronmon wait-on
```

- [`concurrently`](https://github.com/open-cli-tools/concurrently): Run multiple commands concurrently. We'll use it to run both the Electron process and the React app in watch mode.
- [`cross-env`](https://github.com/kentcdodds/cross-env): Run scripts that set and use environment variables across different platforms. We'll use it to make our scripts compatible with both Unix and Windows OSes.
- [`electron`](https://www.electronjs.org/): The core framework for creating the app.
- [`electronmon`](https://github.com/catdad/electronmon): Like [`nodemon`](https://github.com/remy/nodemon), but for the Electron process. Allows watching and reloading our Electron app.
- [`wait-on`](https://github.com/jeffbski/wait-on): Utility to wait for files, ports, sockets, etc. We'll use it to wait for the React app to be built before we open the Electron app (while developing).

The next step is creating Electron's main script. This script controls the main process, which runs in a full Node.js environment and is responsible for managing your app's lifecycle, displaying native interfaces, performing privileged operations, and managing renderer processes.

Create a new `electron.js` file in `public/`:

```js
// Module to control the application lifecycle and the native browser window.
const { app, BrowserWindow } = require("electron");
const url = require("url");

// Create the native browser window.
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600
  });

  // In production, set the initial browser path to the local bundle generated
  // by the Create React App build process.
  // In development, set it to localhost to allow live/hot-reloading.
  const appURL = app.isPackaged
    ? url.format({
        pathname: path.join(__dirname, "index.html"),
        protocol: "file:",
        slashes: true,
      })
    : "http://localhost:3000";
  mainWindow.loadURL(appURL);

  // Automatically open Chrome's DevTools in development mode.
  if (!app.isPackaged) {
    mainWindow.webContents.openDevTools();
  }
}

// This method will be called when Electron has finished its initialization and
// is ready to create the browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  createWindow();

  app.on("activate", function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

// Quit when all windows are closed, except on macOS.
// There, it's common for applications and their menu bar to stay active until
// the user quits  explicitly with Cmd + Q.
app.on("window-all-closed", function () {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
```

Then, we need to make a few changes to `package.json`:

- Rename the app from `@my-app/web` to `@my-app/electron`.
- Add the `main` entry. During execution, Electron will look for the script we created above in the `main` field of the app’s `package.json`.
- Update the `homepage` property. We need to enforce Create React App to infer a relative root path in the generated HTML file. This is a requirement because we're not going to serve the HTML file; it will be loaded directly by Electron.
  To do so, we can set the `homepage` property of the `package.json` to `./` (see [Building For Relative Paths](https://create-react-app.dev/docs/deployment/#building-for-relative-paths) in the Create React App documentation for more details).
- Define a script to build the Create React App and start the Electron process in watch mode.

```diff 
 {
-  "name": "@my-app/web",
+  "name": "@my-app/electron",
   "version": "0.0.0",
   "private": true,
+  "homepage": "./",
+  "main": "./public/electron.js",
   "scripts": {
-    "start": "craco start",
+    "start": "concurrently -k \"cross-env BROWSER=none craco start\" \"wait-on http://localhost:3000 && electronmon .\"",
     "build": "craco build"
   },
```

The `start` script might look a bit confusing now, so here's a breakdown of what it does:

- `concurrently -k ` invokes the subsequent commands in parallel, and kills both of them when the process is stopped.
- `cross-env BROWSER=none yarn start` sets the `BROWSER=none` environment variables (using `cross-env` for Windows compatibility) to disable the automatic opening of the browser and invokes the `start` script, which runs the Create React App build in watch-mode.
- `wait-on http://localhost:3000 && electronmon .` waits for the Create React App dev-server to serve the app on localhost:3000, and then invokes `electronmon .` to start the Electron add in watch-mode.

> ⚠️ Adding a `build` script to our Electron app requires some additional work that, for the sake of simplicity, I glossed over in this blog post. Please check ["Building a desktop application using Electron and Create React App"](https://mmazzarolo.com/blog/2021-08-12-building-an-electron-application-using-create-react-app/) for a more in-depth guide.

Finally, add the `electron:start` script to the root `package.json`:

```json
"scripts": {
  "electron:start": "yarn workspace @my-app/electron start"
},
```

And run it to start developing your Electron app:

![](https://mmazzarolo.com/static/d1674fc224c5887f87fcfdbc9ad70580/b1001/screenshot-electron.png)

## Browser Extension

Extensions, or add-ons, can modify and enhance the capability of a browser.  
There are two primary standards used for building browser extensions:

- [Chrome/Chromium's extension API](https://developer.chrome.com/extensions), supported by Chromium-based browsers (such as Google Chrome, Microsoft Edge, Opera, Vivaldi)
- The [WebExtensions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions), supported by Firefox addons (and, in a limited way, by the latest version of Safari).

These two technologies are, to a large extent, compatible.
In most cases, extensions written for Chromium-based browsers run in Firefox [with just a few changes](https://extensionworkshop.com/documentation/develop/porting-a-google-chrome-extension/).

Extensions are created using web-based technologies: HTML, CSS, and JavaScript. They can take advantage of the same web APIs as JavaScript on a web page, but extensions also have access to their own set of JavaScript APIs.

Since we already have a working web app, we just need a couple of tweaks to use it as the foundation for our browser extension.

Let's start by duplicating the React Native for Web workspace (`packages/web`) into a new `packages/browser-ext` one.

From the `packages/` directory, run:

```bash
cp -R web browser-ext && cd browser-ext
```

Every browser extension requires [a manifest (`manifest.json`)](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension) to be identified by the browser. A manifest contains basic metadata such as its name, version, and the permissions it requires. It also provides pointers to other files in the extension.

By default, Create React App creates a [Web App manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) in the `/public` dir. This default manifest is part of the technologies that power [Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA) and follows an entirely different standard from the Extension API manifest we need.

So, let's replace the content of `public/manifest.json` with our own extension manifest.
This new manifest tells the browser we're building a [popup extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/#user_interface) and that its entry point is located at `browser-ext/public/index.html`:

```json
{
  "name": "My Extension",
  "version": "1.0.0",
  "manifest_version": 2,
  "browser_action": {
    "default_popup": "index.html"
  }
}
```

Then, we need a tiny tweak for the `start` and `build` scripts:

Out-of-the-box, Create React App embeds an inline script into `index.html` of the production build.  
This is a small chunk of Webpack runtime logic used to load and run the application, which is embedded in our `build/index.html` file to save an additional network request on web apps. Unfortunately, it also breaks the extension usage by violating the web extension API Content Security Policy (CSP), which doesn't allow loading external scripts into the extension.  
The easiest way to solve this issue is by turning off the inline script by the [`INLINE_RUNTIME_CHUNK`](https://facebook.github.io/create-react-app/docs/advanced-configuration) environment variable to `false`:

```diff
 {
-  "name": "@my-app/web",
+  "name": "@my-app/browser-ext",
   "version": "0.0.0",
   "private": true,
   "scripts": {
-    "start": "craco start",
+    "start": "INLINE_RUNTIME_CHUNK=false craco start",
-    "build": "craco build",
+    "build": "INLINE_RUNTIME_CHUNK=false craco build"
   },
```

Finally, add the `start` and `build` script to root's `package.json`:

```json
"scripts": {
  "browser-ext:start": "yarn workspace @my-app/browser-ext start",
  "browser-ext:build": "yarn workspace @my-app/browser-ext build"
},
```

We can now run `browser-ext:start` and add the browser extension to the browser to develop it (see ["Install and manage extensions"](https://support.google.com/chrome_webstore/answer/2664769?hl=en) for details):

![](https://mmazzarolo.com/static/b78a911679688706f21c5aa51a5aa831/b1001/screenshot-browser-ext.png)

What we've done so far is just the bare minimum work required to make the browser extension run.  
As your next step, I'd suggest you to:

- Clean up the `public` dir, making sure to keep there only `manifest.json` and `index.html`.
- Remove the Service Worker and the Web Vitals scripts installed by default by Create React App (they won't work in a browser extension).
- Tweak the `start` script to [enable hot-reloading](https://mmazzarolo.com/blog/2019-10-19-browser-extension-development/).
- Get familiar with the browser extension APIs (and limitations).

## Compatibility and platform-specific code

As always, please keep in mind that every platform has its limitations.  
Be it Electron or a browser extension, we shouldn't expect every API exposed by React Native for Web to work out-of-the-box.

Something worth noticing is that, even if we're targeting different platforms/frameworks, the React Native `Platform` API will always detect the OS as `"web"` because it's not aware of whether a React Native for Web app is running in a website, in Electron, or in a browser extension.  
A possible workaround for this issue is to inject a more specific target platform as an environment variable:

```diff
 const webpack = require("webpack");
 const { getWebpackTools } = require("react-native-monorepo-tools");

 const monorepoWebpackTools = getWebpackTools();

 module.exports = {
   webpack: {
     configure: (webpackConfig) => {
       // Allow importing from external workspaces.
       monorepoWebpackTools.enableWorkspacesResolution(webpackConfig);
       // Ensure nohoisted libraries are resolved from this workspace.
       monorepoWebpackTools.addNohoistAliases(webpackConfig);
       return webpackConfig;
     },
     plugins: [
       // Inject the "__DEV__" global variable.
       new webpack.DefinePlugin({
         __DEV__: process.env.NODE_ENV !== "production",
       }),
+      // Inject the "__SUBPLATFORM__" global variable.
+      new webpack.DefinePlugin({
+        __SUBPLATFORM__: JSON.stringify("electron"), // Or "browser-ext"
+      }),
     ],
   },
 };
```

In the `app` workspace, we can then check the `__SUBPLATFORM__` global variable to detect whether we're running in a web page, in Electron, or in a browser extension.

## What's next?

When I started writing this series, I envisioned this post as the last one of the tutorials.  
Still, in the next few days I'll write a FAQs post to ensure the most common questions and answers about the series are captured in a single location. So, please, stay tuned!

If you somehow managed to read through this entire series, hats off to you!  
I hope what I've shown you may give you some ideas about approaching a multi-platform project of your own.  
I surely learned a lot while experimenting with it.

Thanks to the React + React Native team and community for building all these fantastic tools! ♥

For feedback and questions, feel free to start a discussion on the [React Native Universal Monorepo's discussions page](https://github.com/mmazzarolo/react-native-universal-monorepo/discussions) or send me a direct message.

- [Overview](https://dev.to/mmazzarolo/running-react-native-everywhere-4gpp)
- [Monorepo setup](https://dev.to/mmazzarolo/running-react-native-everywhere-yarn-workspaces-monorepo-3j5g)
- [Android & iOS](https://dev.to/mmazzarolo/running-react-native-everywhere-android-ios-3451)
- [Windows & macOS](https://dev.to/mmazzarolo/running-react-native-everywhere-windows-macos-7lp)
- [The web](https://dev.to/mmazzarolo/running-react-native-everywhere-the-web-h6d/)
- **Browser extensions & Electron (☜ you're here)**

> _Originally published at [mmazzarolo.com](https://mmazzarolo.com/blog/2021-09-25-running-react-native-everywhere-electron-browser-ext/)_