loading...
Cover image for Create Your First React Desktop Application in Electron with Hot-Reload

Electron React Create Your First React Desktop Application in Electron with Hot-Reload

jsmanifest profile image jsmanifest Originally published at jsmanifest.com Updated on ・7 min read

Find me on medium
Join my newsletter

If you're a JavaScript developer you might have had most (or all) of your experience building web applications especially with all of these hot new trending technologies being released in every corner within the JavaScript community over the past few years. This might have had an impact on what you decided to develop with over the years.

When we're surrounded by a constantly changing environment in the JavaScript ecosystem that is mostly involved with apps in the web, we might even question whether desktop applications are even worth to get into. A stackoverflow survey for 2019 revealed that there is still a good percentage of desktop developers out there ranging to about 21% of 90,0000 survey participants that are developers, worldwide.

In other words, developing apps in the desktop are still an ongoing popular choice. And so if you were wondering on how to begin with creating your first modern desktop application using JavaScript, then I hope this post will help you to get started with writing your own desktop software application!

We will be using Electron as our main tool. We will also be installing React as we will be using it to get started with building our user interfaces.

The first thing we are going to do is to create our project using create-react-app which will be used to provide react and some other useful tools like Jest for running tests. For this tutorial, I will call our project electron-react-typescript-app:

If you want to grab your copy of the repo, visit this link

npx create-react-app electron-react-typescript-app`

Running that will create the electron-react-typescript-app folder and install the necessary dependencies listed in package.json.

Now let's go ahead and clean up the files we won't be needing. This is how my directory ended up looking like:

cleanup1.jpg

And here is our App.js component:

import React from 'react'

function App() {
  return <h1>Our Electron App</h1>
}

export default App

Now we will go ahead and install electron as a dependency:

npm i electron

And then we will install electron-builder, a complete solution to package and build a ready for distribution Electron app with auto update support out of the box.

Install it as a dev dependency:

npm i -D electron-builder

Note: -D is just an alias for --save-dev

Go ahead and create a "build" property in package.json since electron-builder will be using that:

{
  "name": "electron-react-typescript-app",
  "version": "0.1.0",
  "private": true,
  "homepage": "./",
  "build": {
    "appId": "some.id.ofyours",
    "directories": {
      "buildResources": "assets"
    },
    "win": {
      "category": "your.app.category.type",
      "iconUrl": "path-to-icon.png"
    },
    "mac": {
      "category": "your.app.category.type",
      "iconUrl": "path-to-icon.png"
    }
  },
  "dependencies": {
    "electron": "^6.0.12",
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-scripts": "3.2.0"
  },
  "scripts": {
     "electron": "electron .",
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "electron-builder": "^21.2.0"
  }
}

You can read all of the available options here.

Note: For projects created using create-react-app, you must put the value to the "homepage" property to "./" so that the paths correctly resolve throughout the app, since CRA automatically produces our files assuming our app is hosted from the server root. This is to ensure that the generated index.html file correctly loads the assets after building. If you're unsure of what this means, just trust me and do it :).

When you run npm run electron it will give an error like this:

electron-start-error.jpg

That's because electron cannot find a file to read from. We can create an electron.js file in the root directory or we can create a start script in the src directory which is more intuitive. Let's create a start.js file in the src directory and write some code to initialize a BrowserWindow with some fixed dimensions:

const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow

const path = require('path')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({ width: 800, height: 600 })

  mainWindow.loadURL(`file://${path.join(__dirname, '../public/index.html')}`)

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

After we've done that we have to add a new property to package.json pointing towards this file so that running npm run electron will guide the program to load up start.js:

{
  "name": "electron-react-typescript-app",
  "version": "0.1.0",
  "private": true,
  "main": "src/start.js",
  "build": {
    "appId": "some.id.ofyours",
    "directories": {
      "buildResources": "assets"
    },

Running npm run electron will now open up a window loading up the loading.html file:

npm run electron

Great! Things are starting to look good now :)

Now let's go ahead and run npm start. The script will now load up a web page of our react code successfully!

npm start

But wait a minute... this is not what we are looking for. We're supposed to be seeing our react code in the electron window, so why are we seeing it in the browser instead?

...that's because we essentially have two different processes going on that are doing different things, that have no idea that the other process exists!

So what we're going to have to do is to make electron point to the web server because it has an API that can load up web pages by URL (read about the API method here). This means that we won't be using the browser anymore since we are building a desktop application and that electron can load up content into each of its windows by giving them URLs. So we can instead use the electron windows (which will have access to node.js modules and the local file system).

To be able to make this happen, we will be installing a useful package to detect if the app is run in development or production mode. If the app is runnning in dev mode, then we will use the web server. If the app isn't then that means we have built the files using electron-builder where we load up the app contents through some executable. That's what electron-builder was for.

Go ahead and install electron-is-dev:

npm i electron-is-dev

Now we are going to go ahead and require this in our electron script and use it like so:

const electron = require('electron')
const app = electron.app
const path = require('path')
const isDev = require('electron-is-dev')
const BrowserWindow = electron.BrowserWindow

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  })

  mainWindow.loadURL(
    isDev
      ? 'http://localhost:3000'
      : `file://${path.join(__dirname, '../build/index.html')}`,
  )

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

The important lines to look at are these:

mainWindow.loadURL(
  isDev
    ? 'http://localhost:3000'
    : `file://${path.join(__dirname, '../build/index.html')}`,
)

Now instead of directly loading up the index.html file, we applied a condition to use the webserver available from CRA in dev mode or proceed to load up the index.html file (when the environment is not development).

You might have also noticed that we no longer used the path ../public/indx.html and instead changed it to ../build/index.html. This is because CRA internally does not process files inside the public folder, but instead moves them over untouched to the build folder. Since the build folder will end up having all the generated files in the end, we had to point to it.

Now go ahead and run npm start, then run npm run electron.

You should now see this:

electron with create-react-app

Hurray!

We get the benefits of hot reloading from CRA right into the electron window along with node.js modules and the local file system environment right into the "web" page. How neat is this?

Oh yeah, if you're seeing a browser tab being opened, try setting BROWSER=none to your package.json script like this:

"start": "cross-env BROWSER=none npm run react-start",

Lets now make the main electron process restart when we make changes to start.js, because currently we only have hot-reloading enabled for the web page UI.

For this, install electron-reload and nodemon:

npm i -D electron-reload nodemon

Change your npm run electron script to this:

"electron": "cross-env NODE_ENV=dev nodemon --exec \"\"electron .\"\"",

And simply just require the electron-reload package in start.js:

const electron = require('electron')
const app = electron.app
const path = require('path')
const isDev = require('electron-is-dev')
require('electron-reload')
const BrowserWindow = electron.BrowserWindow

And now the electron window should restart itself when you make changes to start.js.

Conclusion

Congrats, you now know how to create a react desktop application in JavaScript using electron! I hope you found this to be valuable and look out for more in the future!

Find me on medium
Join my newsletter

Discussion

pic
Editor guide
Collapse
baso53 profile image
Sebastijan Grabar

Just a heads up, your naming is wrong, there is no TypeScript in this article. :)

Collapse
maacpiash profile image
Ahad Chowdhury

I wish I noticed this comment before starting to follow this article 😢

Collapse
mphokgosisejo profile image
Mpho Kgosisejo

Hello there...

I've been researching this topic for sometime now (running react on electron), and everytime this is the same solution I always find.

To be clear, what I really need is to access electron modules on the UI (ReactJS) like: dialogs, notifications, and many more system modules electron exposes... however I can not since React runs in a different process, and it (React) does not recognise electron libraries/packages.

The best solution I went with, is to include react libraries and babel through "index.html of electron" (basically I'm not running React on NodeJS, I'm running it within electron)... "I hope I'm making sense".

With this in place, react is now running on the same process as electron and it works fine but I still don't like it. "It's ugly 😭".

My question is, how can one make this work (accessing electron modules on the UI of react and react must use ES6 because I'm using pure JavaScript... and all my components imports are done on electron's index.html - "ugly")... ?

Collapse
jsmanifest profile image
jsmanifest Author

Hello there. You can use electron modules in the renderer process by sending messages through the ipc module. Have you also tried the remote module from electron?

You can also use electron-util in your project and use the api object in any renderer process:

import * as electronUtils from 'electron-util'

const { api } = electronUtils

function quitApp() {
  api.app.quit()
}

Collapse
pkinnucan profile image
Paul Kinnucan

As far as I can determine, this CRA-based approach to creating an electron app will not work for electron apps that need to access the local file system, which is just about every electron app. The reason the CRA-based approach doesn't work for electron is because CRA's webpack implementation mocks out the Node fs module, on which electron relies. The CRA team says that the reason CRA mocks out fs is that CRA is intended for web applications that do not access the local file system and therefore have no need for fs. This leads to a

TypeError: fs.existsSync is not a function

error in any module that uses ipc or remote.

I have tried restoring fs in an ejected version of CRA. This works in production mode but not in development mode. The reason is that the webpack hot module reloader runs in a server and therefore errors out on any app module that uses fs.

All of this illustrates the basic problem with using boilerplates like CRA to set up a project. They all have hidden assumptions that can stop you cold after you have spend hours or days developing an app based on one of them.

Thread Thread
pkinnucan profile image
Paul Kinnucan

After giving up on the CRA-based approach described here, I tried the electron-webpack approach for my react-redux-typescript-based app. Everything went smoothly. My app, which is a pretty complex, hierarchical diagram viewer, worked as expected--in development mode.

However, the production build failed to execute redux-based updates. After many hours of experimentation and surfing, I found out the reason on the electron-webpack issues site. See

github.com/electron-userland/elect...

Turns out that decisions made by electron-webpack developers break redux-react. Fortunately, the suggested workaround works. My app now works the same in production mode as in development mode. However, I am now leery of this boilerplate solution as well.

Thread Thread
weslleysauro profile image
Weslley Rocha

I could access the fs module via de window.require('electron').remote then get the fs .

const app = window.require('electron').remote
const fs = app.require('fs')

export const saveFile = () => {
  fs.writeFileSync('./test.txt', 'hello-world')
}
Collapse
mphokgosisejo profile image
Mpho Kgosisejo

Ok will try it out

Collapse
jrpickhardt profile image
Jeff Pickhardt

So many questions:
1 - What's the point of serviceWorker.unregister()?
2 - There's no TypeScript, right? I downloaded the repo and don't see TS
3 - What's the point of running a development server, then loading the app through the dev server? I am new to Electron but thought you're supposed to have it all contained in some code the Electron app just presents by itself.
4 - What would you run to build it as a production-ready app you could send to others? (I'm curious how this relates to 3 with there being a development server)

Collapse
pkinnucan profile image
Paul Kinnucan
  1. Don't know but the comment in the file provides a link to an explanation.
  2. CRA supports TypeScript but you have to install the typescript compiler and change extensions from js to tsx. Also, if you want to use TypeScript in your Jest tests, you need to install the Jest type definitions package: @types/jest
  3. The development server speeds development by rendering changes to the source without requiring you to restart electron. In production mode, the start.js file loads the browser window with the index.html file that is packaged into the installer generated by electron-build.
  4. Unfortunately, the instructions stop short of explaining how to use electron-build to package your app into an installer that you can distribute to users. To do, this you must add the following scripts to your package.json file:

    "pack": "electron-builder --dir",
    "dist": "electron-builder"

You must also move electron from the dependencies property in your package.json to the devDependencies property.

One other thing, if you want to follow the suggestion about how to avoid popping the dev server up in a browser, you must install cross-env.

Collapse
johannel00 profile image
johannel00

I was getting errors about the entry point. I added this to the "build:" property in the package.json file:

    "files": [
      "node_modules/**/*",
      {
        "from": "src",
        "to": "build",
        "filter": "**/*"
      }
    ]

Here is the source to the comment that helped.

Collapse
jamiebullock profile image
Jamie Bullock

Thanks for posting this. Super helpful! One thing, you might want to mention that "electron": "electron ." needs to be added to package.json to run electron. I didn't spot this and it took a bit of time to figure it out.

Collapse
yannick_rest profile image
Yannick Rehberger

npx did not work for me on macOS:

npx create-react-app foobar

My npm version is 6.14.4, so it's greater then 5.2 and npx should work, right?
However, the old way worked:

npm install -g create-react-app
create-react-app foobar
Collapse
chrismatheson profile image
Chris Matheson

Nice article @jsmanifest , i was wondering if you have ever tried adding electron tests to a "standard" CRA application. I'm going to deploy my CRA based app using electron and some small parts of the codebase will be platform specific, but i can't as yet find a decent way to test them without ejecting because there is no way to specify alternate Jest runners with CRA standard.

Collapse
serbroda profile image
Danny Rottstegge

Hi! Great article.
But I'm trying to have only typescript files and have a start.ts file. The react-script bundle it as chunk files. How do I point a /build/main.js after bundled it with react-scripts in my package.json?
Thx in advice!

Collapse
chrisachard profile image
Chris Achard

Oh, nice! I was just wondering if hot reloading was possible with electron - thanks!

Collapse
jsmanifest profile image
jsmanifest Author

Your welcome!

Collapse
rotirotirafa profile image
Rafael Rotiroti

where my exe files is it?

Collapse
anjayluh profile image
Angella Naigaga

Hi, this article is really easy to follow. Can you add some steps on how to create an executable file after all this or share a link that could help?