DEV Community

Cover image for Electron Apps Made Easy with Create React App and Electron Forge
Mandi Wise
Mandi Wise

Posted on

Electron Apps Made Easy with Create React App and Electron Forge

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.

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:

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:

cd cra-electron-forge-demo

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

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:

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.

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:

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, 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:

{
  "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 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:

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:

{
  // ...
  "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

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. We can import our existing Electron app into the Electron Forge workflow by running this command in our project now:

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:

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

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

{
  // ...
  "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:

{
  // ...
  "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:

{
  // ...
  "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):

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:

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:

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:

// ...

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

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:

{
  "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:

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:

{
  // ...
  "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

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

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:

# ...

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

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

Top comments (33)

Collapse
 
alfonsodev profile image
Alfonso • Edited

Thank you! Excellent guide!
A note for those using react-router 6, you'll need to replace BrowserRouter with HashRouter or you'll get a white screen when building for production.

Collapse
 
waliddamou profile image
waliddamou

omg you saved my life bro thank you <3

Collapse
 
tuncatunc profile image
tuncatunc

Thanks for the tip Alfonso

Collapse
 
celludriel profile image
Kenny De Smedt • Edited

Well written article helped me with the setup however I still had to do:

npm i -D @electron-forge/maker-squirrel
npm i -D @electron-forge/maker-zip
npm i -D @electron-forge/maker-deb
npm i -D @electron-forge/maker-rpm

Then I ran into other problems, like the file folder was to long and such. I had to fix it by adding

// ....
  "config": {
    "forge": {
      "packagerConfig": {
        "asar": true
      },
// ....
Enter fullscreen mode Exit fullscreen mode

Another thing with this setup although it works the electron app is 300mb .... this is a lot there are probably ways on making it smaller, this could be part of this article or a new one maybe ?

Collapse
 
ririshi profile image
Ririshi • Edited

Hey Mandi,

I just happened to be working on moving our existing React dashboard from electron-builder to electron-forge, when I stumbled upon your perfectly timed article. I'm running into some issues with setting up the electron forge building process.

We've used create-react-app to set up the application initially, and are now using a folder structure where the src folder contains all our sources. There's also a public folder (from CRA), and we have some extra folders for unit tests and a bunch of files to configure things like GitLab CI, Babel, GraphQL, etc. CRA builds the application for us to the build folder, but when using electron forge to package / make the app, it will copy literally everything in the root folder (including tests, node modules, and our project config files) into the packaged app.

I've been looking for hours on Google to find out how I could specify a folder (e.g. the build folder) for Forge to use, but it seems like that's not possible at all. For the time being, I've just put every non-important folder to the ignore key of the packager config, but that's not a very solid solution.

Hoping you know a bit more about this than I do, and that you might be able to help me out!

Edit: I also just found out that CRA copies the public files into the build folder, but Electron Forge will still use the original files it copied over in the public folder, so you essentially have those files packaged twice in the final executable.

Collapse
 
mandiwise profile image
Mandi Wise

Hi Ririshi! This is a bit outside the scope of what I've done with Electron Forge, but if I discover a solution as I continue to experiment with it, then I'll post it here.

Collapse
 
gabrielalvescunha profile image
Gabriel Alves Cunha

Have u found nothing? :(

Collapse
 
chiefkes profile image
Max Liefkes

Any luck resolving this?

Collapse
 
seeingblue profile image
SeeingBlue

Install electron-forge globally and run electron-forge init electron-forge-react --template=react

Collapse
 
celludriel profile image
Kenny De Smedt

electron-forge v6 does no longer have a react template

Collapse
 
raibtoffoletto profile image
RaΓ­ B. Toffoletto

Please do not install npm packages globally... safety first. Npx is there to help πŸ˜‰

Collapse
 
brittanynavin profile image
Brittany Navin

I'm using the latest version (v6.0.0) of the wait-on npm package and my electron app didn't ever seem to pop up properly after my react app started up.

To investigate, I started up my react app on port 3000 and then cd'ed into my node_modules/wait-on/bin directory and ran ./wait-on tcp:3000 -v. I was getting the following error.

making TCP connection to 3000 ...
  error connecting to TCP host:localhost port:3000 Error: connect ECONNREFUSED ::1:3000
Enter fullscreen mode Exit fullscreen mode

My react app was 100% running on localhost:3000, as I could access it in my browser.

I stopped that running wait-on process and re-ran wait-on, passing my "localhost" IP into the wait-on command like so: ./wait-on tcp:127.0.0.1:3000 -v

Finally then got TCP connection successful to host:127.0.0.1 port:3000

So i modified the electron script in my package.json as follows:

    "electron": "wait-on tcp:127.0.0.1:3000 && electron .",
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bladnman profile image
Matt Maher

Great debugging here. Saved me.

Collapse
 
toggenation profile image
James McDonald

Thanks Brittany,

Was getting the exact same issue trying to start electron.

Appreciate the post :)

Collapse
 
kltliyanage profile image
kltliyanage

you can't use Browser= none without using cross env

It gets an error: npm ERR! app@0.1.0 dev: concurrently -k "BROWSER=none npm start" "npm:electron"

npm install --save cross-env
"start": "cross-env BROWSER=none react-scripts start",

Collapse
 
derekdavie profile image
DerekDavie • Edited

Very helpful thank you!

I ran into one issue I wanted to document in case anyone else ran into it too. When building I was getting a blank screen and it took me awhile to figure out why. In electron.js I changed:
win.loadURL(
isDev
? "localhost:3000"
: file://${path.join(__dirname, "../build/index.html")}
);

to

win.loadURL(
isDev
? "localhost:3000"
: file://${path.join(__dirname, "/../build/index.html")}
);

and it was able to launch my app correctly!

Collapse
 
emuswailit profile image
emuswailit

Apparently this happens when you skip the "homepage":"./" bit in the tutorial. Honestly after days and days of exploring vite-electron, electron react boilerplate, etc this tutorial was the most helpful to me. Not forgetting the sister tutorial on electron sqlite. Thank you @mandiwise ...in 2024!

Collapse
 
macduff111 profile image
macduff111

Hi
thanks for this it's really useful. I can get the dev version running no problem, but when I try and package with npm run make I get the following error:

Preparing to Package Application for arch: x64
An unhandled rejection has occurred inside Forge:
{ Error: EEXIST: file already exists, mkdir '/var/folders/jh/7xh_52cx65566d0fhgf01z6h0000gn/T/electron-packager/darwin-x64-template'
  errno: -17,
  code: 'EEXIST',
  syscall: 'mkdir',
  path: '/var/folders/jh/7xh_52cx65566d0fhgf01z6h0000gn/T/electron-packager/darwin-x64-template' }

Electron Forge was terminated. Location:
{}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! cra-electron-forge-demo@0.1.0 make: `react-scripts build && electron-forge make`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the cra-electron-forge-demo@0.1.0 make script.
Enter fullscreen mode Exit fullscreen mode

I tried Googling, but couldn't find any similar problem. Any ideas?
Thanks

Collapse
 
macduff111 profile image
macduff111

I updated npm and all packages and it now runs OK

Collapse
 
carlosthe19916 profile image
Carlos E. Feria Vila

Nice blog. I have one question though: hot reloading seems to work for the ReactJS application but not for Electron; I mean, once any change is done in the electron.js file then the changes do not take effect until stop and run again the "dev" command. Any suggestion on how to add Hot reload also to the Electron code?

Collapse
 
mariusionescu profile image
Marius Ionescu

This is excellent! Many thanks!

One comment, on windows cross-env needs to be added for BROWSER=none to work correctly.

Collapse
 
eyuzwa profile image
Erik Yuzwa

likewise! I went with "dev": "concurrently -k \"cross-env BROWSER=none npm start\" \"npm:electron\""

Collapse
 
k2code profile image
Kimita Kibana Wanjohi

or you can simply add it to your projects .env file. it works the same

Collapse
 
oallouch profile image
Olivier Allouch

I'm also on Windows and I use a .env file.

Collapse
 
kurotsuba profile image
Kurotsuba

Thank you! This article helps me a lot!

While importing electron-forge with

npx @electron-forge/cli import

I got an error:

could not determine executable to run

I'not sure if this is related to npm version, my version is 8.3.1

finally I solve this with the command below

npm  i -D @electron-forge/cli
npx electron-forge import
Enter fullscreen mode Exit fullscreen mode

This solution comes from electronforge.io/import-existing-p...

Collapse
 
xam_mlr profile image
Xam-Mlr

Hi Mandi,
Thanks to your tutorial my app was working fine but since i've updated macOS and Node it's not...
Nothing happen anymore when electron is supposed to start.
Do you know where it could come from ?

Collapse
 
xam_mlr profile image
Xam-Mlr

It would come from wait-on which is always refused when starting a connection even when react is started

Collapse
 
tombohub profile image
tombohub

Thanks,

what is package what is make for forge?

why do we have to run start with electron-forge start?

Collapse
 
andrewbaisden profile image
Andrew Baisden

Good post I really need to give this a go before I try this with Flutter and React Native.

Collapse
 
tuncatunc profile image
tuncatunc

Many thanks for the great blog post πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ™ŒπŸ™ŒπŸ™ŒπŸ™Œ