Electron Apps Made Easy with Create React App and Electron Forge
Tags: react, javascript, electron
Published: 2020-07-28 16:04:36

If you ever need to wrap a React app in Electron, you'll surely find there's no shortage of tutorials that explain how to set up your project in your local development environment. But what happens when you're ready to package up that app in distributable formats so users can install it on their systems?

I recently found myself needing to do this on a project and found that I had to do a bit of experimentation before setting on how to best approach generating, developing, and packaging an Electron app with code scaffolded from Create React App under the hood.

In this post, I'll share with you what steps I followed to make this happen and how I reconciled the code generated by the CRA and Electron Forge CLIs to run no-fuss builds with a single command.

**TL;DR:** You can find the [complete code from this demo here](https://github.com/mandiwise/cra-electron-forge-demo).

## Set Up the React App with Electron

The approach we'll take to set up our app will involve a few steps:

1. Scaffold a new React app using the CRA CLI tool
2. Add a file to start up an Electron app with the React app running inside of it
3. Import the project into the Electron Forge workflow so that it can easily be packaged up for distribution

We'll take care of the first two steps in this section and then configure our project with Electron Forge in the next section. Let's begin by creating the new React app:

```sh
npx create-react-app cra-electron-forge-demo --use-npm
```

I'll use npm in this tutorial so I've passed the `--use-npm` flag above, but you can use Yarn if you prefer too. Now we'll change into our new project directory:

```sh
cd cra-electron-forge-demo
```

Our next step will be to install Electron in our project as a development dependency:

```sh
npm i -D electron@9.1.1
```

And we'll also install a package that will make it easy for us to detect if we're running in a development or production environment in our Electron app:

```sh
npm i electron-is-dev@1.2.0
```

Moving onto the second step, we're going to add a file to the `public` directory called `electron.js` to contain all of our Electron-related code. 

```sh
touch public/electron.js
```

Note that you will often see this file called `main.js` in tutorials, but I think that calling it `electron.js` here disambiguates what it's purpose is!

Inside of `electron.js`, we'll add the following code:

```js
const path = require("path");

const { app, BrowserWindow } = require("electron");
const isDev = require("electron-is-dev");

function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  });

  // and load the index.html of the app.
  // win.loadFile("index.html");
  win.loadURL(
    isDev
      ? "http://localhost:3000"
      : `file://${path.join(__dirname, "../build/index.html")}`
  );

  // Open the DevTools.
  if (isDev) {
    win.webContents.openDevTools({ mode: "detach" });
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(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", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  // 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();
  }
});

// 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.
```

The code above has been adapted from a starter example in the [Electron docs](https://www.electronjs.org/docs/tutorial/first-app#installing-electron), but with a few notable changes:

- We use the `electron-is-dev` package to conditionally point Electron to either our local development server or a bundled production version of our React app (and we use Node's `path` module to help with this).
- We conditionally open the Chrome dev tools depending on the environment (because we only want them to open automatically in development).
- Lastly—and this a matter of personal preference—we detach the dev tools from the app's main window when it launches. If you would prefer to keep the dev tools attached to the main window, then you can instead leave the object argument out when calling the `win.webContents.openDevTools` method.

With this file in place, we'll need to declare it as the main entry point for the Electron app in our `package.json` file:

```json
{
  "name": "cra-electron-forge-demo",
  "version": "0.1.0",
  "main": "public/electron.js", // NEW!
  // ...
}
```

Now we need to think about how we're going to start our app up. In the development environment, we'll want to start the React app up first and only launch Electron once [http://localhost:3000](http://localhost:3000) is available. Ideally, we'd also prevent the default CRA behavior that opens a browser tab with our app running in it. (We don't have to worry about any this in production because Electron will just load the static `index.html` and bundled JS files.) 

To launch our development server and Electron together, we'll need to install two more packages:

```sh
npm i -D concurrently@5.2.0 wait-on@5.1.0
```

Concurrently will allow us to run multiple commands in one npm script and wait-on will require Electron to wait for port 3000 to be available before the app launches. We'll use these packages to add `dev` and `electron` scripts to our `package.json` file:

```json
{
  // ...
  "scripts": {
    "dev": "concurrently -k \"BROWSER=none npm start\" \"npm:electron\"",
    "electron": "wait-on tcp:3000 && electron .",
    // ...
  }
  // ...
}
```

Passing the `BROWSER=none` option before `npm start` will prevent a regular browser tab from launching once our React app starts up. If we run `npm run dev` now, then we'll be able to see our React app running with Electron instead of in a browser window:

![Create React App running inside of an Electron window](https://dev-to-uploads.s3.amazonaws.com/i/j8awc0jitq2kfh77hoqe.png)

## Configuring Electron Forge

If all we wanted to do was wrap a React app in Electron and experiment with it in our development environment, then we could call it a day here. But odds are that you'll likely want other people be to able to use your app like any other desktop app on their computers too!

There are different options available for packaging up Electron apps for different platforms, but the best all-in-one solution I've tried is [Electron Forge](https://www.electronforge.io/). We can import our existing Electron app into the Electron Forge workflow by running this command in our project now:

```sh
npx @electron-forge/cli import
```

When the importer script runs (and it may take a couple of minutes...), Electron Forge will install some additional dependencies and make some changes to our `package.json` file. If we take a look at the `scripts` in the `package.json` file, we'll see that it has modified our existing `start` script to this:

```json
{
  // ...
  "scripts": {
    // ...
    "start": "electron-forge start",
    // ...
  }
  // ...
}
```

We'll need to change this script back to the way it was before:

```json
{
  // ...
  "scripts": {
    // ...
    "start": "react-scripts start",
    // ...
  }
  // ...
}
```

We'll also make a small adjustment to the new `package` and `make` scripts to also build our React app beforehand:

```json
{
  // ...
  "scripts": {
    // ...
    "package": "react-scripts build && electron-forge package",
    "make": "react-scripts build && electron-forge make"
  }
  // ...
}
```

Lastly, we'll modify our own `electron` script to use `electron-forge` to start up the Electron app instead:

```json
{
  // ...
  "scripts": {
    // ...
    "electron": "wait-on tcp:3000 && electron-forge start",
    // ...
  }
  // ...
}
```

Over in `public/electron.js` now, we can use the `electron-squirrel-startup` package installed Electron Forge to handle creating and removing shortcuts on Windows (should we wish to package our app for that platform too):

```js
const path = require("path");

const { app, BrowserWindow } = require("electron");
const isDev = require("electron-is-dev");

// Handle creating/removing shortcuts on Windows when installing/uninstalling
if (require("electron-squirrel-startup")) {
  app.quit();
} // NEW!

// ...
```

With our new code in place, we should be able to run `npm run dev` again and see the Electron app start just as it did before.

## Add Support for React Developer Tools

If we're building a React app, there's a good chance that we're going to want to have access to the React Developer Tools extension at some point. We'll need to add support the React Developer Tools manually to our app by installing this package:

```sh
npm i -D electron-devtools-installer
```

Next, we'll add some new code at the top of `public/electron.js` to conditionally require this package when our development environment is detected:

```js
const path = require("path");

const { app, BrowserWindow } = require("electron");
const isDev = require("electron-is-dev");

// Conditionally include the dev tools installer to load React Dev Tools
let installExtension, REACT_DEVELOPER_TOOLS; // NEW!

if (isDev) {
  const devTools = require("electron-devtools-installer");
  installExtension = devTools.default;
  REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS;
} // NEW!

// Handle creating/removing shortcuts on Windows when installing/uninstalling
if (require("electron-squirrel-startup")) {
  app.quit();
} // NEW!

// ...
```

Lastly, we'll need to call the `installExtension` function when the app is ready, so we must update the existing `app.whenReady().then(createWindow);` lines as follows:

```js
// ...

app.whenReady().then(() => {
  createWindow();

  if (isDev) {
    installExtension(REACT_DEVELOPER_TOOLS)
      .then(name => console.log(`Added Extension:  ${name}`))
      .catch(error => console.log(`An error occurred: , ${error}`));
  }
}); // UPDATED!

// ...
```

 If we relaunch the app now, we should be able to see that the Components and Profiler tabs are available in the dev tools window:

![React Developer Tools running in an Electron app](https://dev-to-uploads.s3.amazonaws.com/i/t69lyn8ns53lqa5abb1x.png)

## Building for Distribution

We're finally ready to generate a platform-specific distributable so other people can install our app. In the following example, we'll build the Mac version of the app.

First, we'll need to set a `homepage` property in the `package.json` file to help CRA correctly infer the root path to use in the generated HTML file:

```json
{
  "name": "cra-electron-forge-demo",
  "version": "0.1.0",
  "main": "public/electron.js",
  "homepage": "./",
  // ...
}
```

As a nice touch, we can also create a custom app icon to appear in the user's dock using the `electron-icon-maker` package. To do this, we'll need to supply it an absolute path to a PNG file that's at least 1024px by 1024px. We'll run this script from the root directory of our project to generate the icon files:

```sh
npx electron-icon-maker --input=/absolute/path/to/cra-electron-forge-demo/src/app-icon.png --output=src
```

Next, we can add the correct icon file and customize the name our app (as it will appear in the top menu or when hovering over the dock icon) under the `config` key in our `package.json` file:

```json
{
  // ...
  "config": {
    "forge": {
      "packagerConfig": {
        "icon": "src/icons/mac/icon.icns",
        "name": "React + Electron App"
      },
      // ...
    }
  }
}
```

Note that if you wish to change the name that appears at the top of the window, you'll need to update that in the `title` element in the `public/index.html` file before building the app:

![Diagram of app menu and window names](https://dev-to-uploads.s3.amazonaws.com/i/rvuo9wb7kflp04fwx9lf.png)

Now we can run a single command to package up our app for distribution:

```sh
npm run make
```

The packaged app can now be found in a new `out` directory in the root of our project. You can now drag this app in your `Applications` folder and run it like any other app on your system.

Finally, if you plan on version controlling this project with Git, then be sure to add the `out` directory to the `.gitignore` file before making your next commit:

```text
# ...

# production
/build
/out # NEW!

# ...
```

## Summary

In this tutorial, we used Create React App to scaffold a React app, which we then wrapped in Electron. We imported the app into an Electron Forge workflow using the provided CLI tool, made some adjustments for it to play nice with a CRA app, and then generated a distribution of the app for Mac.

You can find the [complete code for this tutorial here](https://github.com/mandiwise/cra-electron-forge-demo).

I hope you've found the code in this tutorial helpful! Thanks for coding along and please leave any questions or comments below.