loading...

How to Setup Simple Hot-Reload on an Electron App With No External Dependencies

polluterofminds profile image Justin Hunter ・5 min read

Update: I created a Github repo with the example code here

I have built Electron apps before, but I wanted to challenge myself to build one without any tutorial and by reading the Electron docs only (and some Stackoverflow, of course). I also wanted to challenge myself to use HTML, CSS, and Vanilla JavaScript. No React. No external libraries.

I immediately ran into a problem.

While developing, I have become accustomed to hot-reload—the automatic refreshing of the content on the screen after I've made changes. You don't get that out of the box with Electron, so I set out to solve it without adding any dependencies. Turns out, it was pretty simple.

The first thing you'll need to do is setup a new Electron project. That's as simple as following their quickstart docs, but I'll outline it here so you don't have to jump between tabs. My instructions are focused on MacOS, but Windows people, I think you can map them to Windows instructions pretty easily.

From the Terminal, create a new folder: mkdir electron-hot-reload.

Then, change into that directory: cd electron-hot-reload.

Now, you'll need to initialize the directory with npm init. Answer the questions that you're prompted to answer. When you're done with that, you'll need to install Electron:

npm i --save-dev electron

Now, open your directory in your favorite code editor. You'll need to possibly make a change depending on how you set things up when running npm init. Check your package.json file and see what file name is indicated in the main property. I am using main.js for my project, so if you'd like to do that again, make sure your package.json looks like this:

{
  "name": "electron-hot-reload",
  "version": "0.0.1",
  "description": "A simple hot-reload example for Electron",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^9.1.2"
  }
}

Now, you can create the main.js file. In your Terminal, run touch main.js && touch index.html. This will create an empty JavaScript file called main.js and it will create the empty file that will host our front-end code. Time to write some code!

In your main.js file, add this:

const { app, BrowserWindow, ipcRenderer, ipcMain } = require('electron')

let win;

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

  // and load the index.html of the app.
  win.loadFile('index.html')
}

app.whenReady().then(createWindow)

You might be asking why we define the win variable outside of the createWindow function but never use it again or reassign it outside that function. Don't worry, we will. Let's get our HTML file set up and make sure Electron runs before we work on the hot reload.

In your index.html file, add this simple boilerplate:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
  </head>
  <body>
    <h1>Hello World!</h1>
    <p>This is a simple hot-reload example for Electron.</p>
  </body>
</html>

Now, to run the app. Go back to your package.json file. There, you'll see a scripts section. Let's add a start script so that section looks like this:

  "scripts": {
    "start": "electron ."
  },

Save that, then back in your Terminal run npm start. Electron should open your new desktop app with the HTML content we included displayed.

Awesome! But now, make a change to that HTML. Nothing happened, right? To see the change, we have to close the app then start it again. That's not very efficient. Let's solve that with hot-reload.

The first thing we need to do is close the app and create a new file called watcher.js. You can do that manually or in the Terminal by running touch wathcer.js. Before we forget, let's make sure we wire that file up to our index.html file so it gets loaded on start. Right before the closing body tag in your index.html add this:

<script src='./watcher.js'></script>

Now, we can add some code to the watcher.js file. Remember, the hot-reload functionality we're building will use no external dependencies. We will simply make use of the built-in Node Filesystem and what we get out of the box with Electron. In your watcher.js file, add the following:

const fs = require('fs');

(async () => {
  const watcher = fs.watch('./index.html');
  watcher.on('change', () => {
    console.log('changed')
  });
})();

Not much going on in this file, but let's walk through it. We're using Node's built-in file system to watch for changes to whatever file we define. In this case, we are defining that file to be our index.html file, but you could see how we might be able to define multiple different files to watch for our hot-reload system. The watcher event handler just listens for changes to the file we defined, and, for now, it console.logs the word "changed".

Now, if you run your Electron app (npm start) and open the developer tools window in your app (click View -> Toggle Developer Tools), and look in the console of the developer tools window, you will be able to watch for changes. Let's try it. In your index.html file, change the heading to say "Hello Electron!". When you save it, you should see in the console of the developer tools window, the word "changed" is printed.

Now, all we need to do is actually update the app to show our changes rather than logging out a word. Close the app, and let's finish this thing up.

Back in your watcher.js file, let's import the ipcRenderer helper from Electron. At the top of the file add this:

const { ipcRenderer } = require('electron');

Then, inside the listener we set up, replace the console.log with ipcRenderer.send('re-render');. That's it for our watcher file. Now, we need to tell Electron what to do when it receives the 're-render' command.

Open up your main.js file, and add the ipcMain import to your other Electron imports:

const { app, BrowserWindow, ipcMain } = require('electron')

Now, beneath the createWindow function, add the following:

ipcMain.on('re-render', () => {
  win.loadFile('index.html')
})

Remember, I told you we'd re-assign that win variable. Well, here you go. We are telling Electron to listen for a 're-render' message. When that message comes through, we are simply reloading our index.html file.

That's it. Run your app again, make a change to the index.html file, and you'll see your change immediately in your app window. Pretty cool, right?

We did this all without Webpack or any other bundling libraries. There are plenty of options for creating hot-reload in Electron (and other apps), but if you need a lightweight solution, this may be the right choice for you.

If you enjoyed this article, please consider subscribing for free to my site where I talk about non-traditional paths to coding, technology, and pretty much anything else I like.

Posted on by:

polluterofminds profile

Justin Hunter

@polluterofminds

Writer, Developer, Builder. Founder of Graphite and SimpleID.

Discussion

markdown guide
 

Obligatory security warning:

Isolation For Untrusted Content

A security issue exists whenever you receive code from an untrusted source (e.g. a remote server) and execute it locally. As an example, consider a remote website being displayed inside a default BrowserWindow. If an attacker somehow manages to change said content (either by attacking the source directly, or by sitting between your app and the actual destination), they will be able to execute native code on the user's machine.

⚠️ Under no circumstances should you load and execute remote code with Node.js integration enabled. Instead, use only local files (packaged together with your application) to execute Node.js code. To display remote content, use the tag or BrowserView, make sure to disable the nodeIntegration and enable contextIsolation.

electronjs.org/docs/tutorial/security

While you are using a local file here, it's worth noting that this can apply if you're loading e.g. JavaScript from a remote source in that file, or remote JavaScript called from a local JS file.

Might also want to strip this from the output if app.isPackaged === true when you package (not just wrap the function to run if false) to ensure it's not running in production. electronjs.org/docs/api/app#appisp....

In the case of Electron, because it has access to Node APIs like fs AND remote frontend code, extra considerations are necessary to keep yourself and your users from getting pwned.

 

Thanks! And totally agree. Always have to keep an eye on the security concerns, especially with Electron apps.

 

This is really cool! I've heard that the fs.watch() can behave inconsistently across different operating systems. Have you experienced any weird behavior? I recently opted for Chokidar in one of my recent projects for that reason, but I always like dropping dependencies when I can.

 

Honestly haven’t tried this on any other OS yet but I’ll be giving it a shot on my wife’s Windows machine soon and will let you know!

 

For those who are interested, I pushed the code up to Github for anyone to pull down and use: github.com/jehunter5811/electron-h...