“JavaScript” for web, mobile, and now, desktop applications too
By: Surya Kant Bansal
Tags: Electron, JavaScript, React Electron, Web Development, Desktop App

Its been a few days since I came upon a project to build a desktop application. I have worked on web and mobile applications in the past using React and React-Native but this was something new. Long ago, I heard about a framework that allows us to create multi-platform desktop applications in JavaScript and I thought to myself that this should be used for this project. That framework was “Electron”!

Electron applications run on two threads, main and renderer. The main thread uses Electron APIs to communicate with the OS to perform native desktop application operations. The renderer thread is responsible for rendering the DOM on the application window. These two threads communicate with each other using the IPC(Inter-process communication) Electron API.

The Electron homepage clearly states that “It’s easier than you think” and it is true for most of it but the setup was not that easy. I was going to use the React for the project so I started out by using the electron-react-boilerplate which was also mentioned in the Electron documentation. The problem that I faced with this boilerplate was that it was too bulky and there were a lot of modules installed that were irrelevant to the cause. I required a boilerplate that had just the minimal dependencies to create a React-based Electron application. After extensive research, I decided that I should create one for myself 😬.

Bringing in the programming:

I started out a simple starter code I found in the documentation. It creates a window and loads an HTML file inside it, which is pretty simple. Next, loading a React application inside that window. I already had a boilerplate for the React application that I created some time ago. Easy things first, I bundled the React application, tried to load the output HTML file in the window but it did not work. I realized I have to make some changes to the Webpack configuration. I use "/" as the publicPath in the Webpack output because that is required when the application is served from a server. In the case of Electron, the files will be picked from the local system, so I removed it. Next, I removed the hash file names, removed the split chunks optimizations and used a single file name i.e. renderer.js , and removed the polyfill because it is not required. After all these changes, I ran the bundle in the Electron application again and EUREKA! it worked.

Development mode is necessary

To the next challenge, running the application in a development mode with HMR(Hot Module Replacement). For the React web application, I use the webpack-dev-server to serve the files and to use HMR. To use the same in Electron, I came across the BrowserWindow.loadURL function that can load a URL inside the Electron application window. Now, I served the React application using the dev server, passed https://localhost:3000/ inside the loadURL function and my application was now running in a development environment. But soon I realized that the HMR was not working so I started to look for a solution. In the electron-react-boilerplate that was mentioned in the documentation, I noticed that they were using the react-hot-loader package too for HMR. I used the same and made sure it was only used in the development build using environment variables set in the Webpack configuration and everything started working smoothly now.

Thread communication

Next up, using the IPC APIs to communicate between threads. I tried to import the ipcRenderer module in the React application from the electron package but that threw an error. Apparently, electron is a node module and any application using it must be running on a node environment but our renderer.js actually runs in a browser environment. After researching this issue, I found that I can set the nodeIntegration flag, in the BrowserWindow options, to true in order to expose the renderer application with the node environment. With this, I was able to use the ipcRenderer module but I later discovered that the renderer application should not be exposed with the node environment because the node environment exposes the file system to an application and any external injected script might be able to access the file system 🤷🏻‍♂️.

I found several solutions that did not work. Finally, I went back to the bulky electron-react-boilerplate and I checked how they handled this issue and I found that they passed the bundled React application JS in BrowserWindow options preload. This actually provides two benefits, first, preload scripts have access to the node environment, and second, it makes the React application load a little faster in the application window. And again EUREKA! this works perfectly. I was able to send and receive messages to and from the main and renderer threads. I created a common.js file too where I can store the communication channel names used by both main and renderer threads.

Getting ready for production

While reading more of the documentation, I found out that Webpack provides specific targets to bundle JS code for Electron. For bundling renderer code, electron-renderer and for bundling preload script, electron-preload . I use electron-renderer in development mode because in that case, I do not preload my React application JS and I set nodeIntegration to true for development. For production, I use electron-preload and set nodeIntegration to false and I also make sure to remove the renderer.js file reference in the index.html , which can be done in the Webpack configuration, because it will be preloaded. This is how my main window configuration looks like:

const mainWindow = new BrowserWindow({

show: false,

backgroundColor: "#000000",

width: 1024,

height: 768,

webPreferences: {

nodeIntegration: isDev,

contextIsolation: !isDev,

enableRemoteModule: false,

preload: isDev ? null : path.join(__dirname, "build/renderer.js"),

},

});

I also need to make sure that in development the HTML is loaded from the dev server running on localhost and in production, it should be loaded as a file.

const APP_URL = isDev ? "http://localhost:3000" : `file://${__dirname}/build/index.html`;

mainWindow.loadURL(APP_URL);

Security advisories

“Electron” has a complete page on all the security advisories that we as developers should follow. I spent a lot of time on this document and almost all of the security advisories have been handled in the boilerplate that I created. There are proper checks as to enable and disable some properties based on the environment in which the application is being run. You can read more about these security advisories here: https://www.electronjs.org/docs/tutorial/security.

Packaging and shipping

For packaging and shipping the application, I could not find a simpler tool than the electron-builder. This tool can package the application, for all the platforms, in one simple command. All you need is to create an electron-builder.yml file, specify some parameters, and then run the command electron-builder build -mwl and it will create application installers for MacOS, Windows, and Linux 🙌🏼. Here is the electron-builder.yml file that I use:

productName: "ElectronReact"

appId: "com.electron.ElectronReact"

files:

- "build/"

- "main.js"

- "common.js"

Setting the icon for the application is simple too. You just need a 512px x 512px size PNG image, name it as icon.png and make sure it is included in the files list in the electron-builder.yml file.

Conclusion

Creating this application boilerplate from scratch has not been easy and honestly starting anything new never is easy. I hope it’ll save a lot of time for the people who use this boilerplate to start new projects.

Thank you for reading this!