Electron and Vite.js with React.js

Electron and Vite.js with React.js

This post was translated using the DeepL translator, so I apologize for any incomprehensibility. In this article you can read more about my decision.

I recently started working on a relatively simple application in Electron. Since I'm used to working with the JavaScript frameworks Vue.js and React.js, I wondered if I could use them for this application - of course I could. So I decided to go with Vue.js. I spent some time Googling what interleaving methods are most commonly used. Most of the time I ended up with some ready-made CLI. So I decided to try Vue CLI plugin Electron Builder. As the name suggests, it is an extension for the Vue CLI and implements, besides Electron, also Electron Builder, which is a package that makes building a build with a finished application much easier.

So I started to develop the application, and tried a few first builds. At the beginning everything seemed to be without problems, these only increased as the application code was extended. Except perhaps that the first build on macOS didn't work for me either, but it turned out to be a problem with the Archiver application, which doesn't restore symlinks after unzipping the zip, so it can be worked around with other archive software. Besides, Apple has known about the bug since macOS Catalina. I'll stick with macOS, I also tried a build for new MacBooks with Apple Silicon processors, at first Electron Builder didn't seem to recognize these processors, however after checking in Activity Monitor I found that some builds are marked as Apple and others Intel. So I figured that the builds are being created according to the correct architecture, but it depends which one is created first - ARM or Intel, because the latter will overwrite it. A number of other silly little things like that popped up, besides not being able to create a .dmg or .pkg. So my friend recommended electron-packager. But replacing the builder in this CLI utility is not a matter of one npm (Node Package Manager) command. I eventually solved all the problems and the builds work the way I need them to, but it did lead me to wonder, what if I solved the whole thing myself?

That is what I would like to address in this article. However, unlike Vue.js, I decided to go with React.js. I've also decided to replace the traditional Webpack with Vite and see if these tools can be combined. Let's leave aside that while with Electron bundler I get some freedom over configuration, with Vite I sacrifice some of it again. However, combining these tools is ultimately easier than I would have expected. Of course, it has its pitfalls and is definitely not as convenient as ready-made CLI tools, unless you decide to write something similar yourself. That won't be part of this article, though.

Vite and React.js

Let's start by installing Vite, as I said it is in a way similar to Webpack. Like Webpack, it also offers a dev server and a tool to build application code. However, unlike Webpack, it works directly with ES (ECMAScript) modules and does not compile them into CommonJS, and the Express.js web server has replaced the minimalist Koa server. The Hot module reloading is also many times faster, de facto instant, on the other hand it allows to use only ES modules and not CommonJS or AMD. Build code is done in a pre-configured Rollup, whereas Webpack handles this itself. A sort of imaginary advantage is also the need for minimal configuration of the whole project, that is if you decide to use one of the predefined presets, for example React or Vue.

1npm init @vitejs/app my-react-app

During project creation Vite asks for a template choice, so we choose react. We are then prompted with a few commands that we should run to get everything installed, ie:

1cd my-react-app
2npm install
3npm run dev

If we've launched at least the first two, the Vite interface and React.js app are ready. That leaves the final command, which is to run the dev server to verify that everything works. We should see in the command line that the app is running on http://localhost:3000 or some other port if 3000 is already taken. We can then proceed to install Electron,

Electron

It is a framework for creating cross-platform desktop applications using JavaScript, CSS and HTML. Thanks to Electron we can create native applications and the hardest things like updating, native menus and notifications or crash reporting are handled by Electron. Installation is very simple, just run the following command:

1npm install --save-dev electron

Now we just need to edit the package.json file and add the entry point for the Electron application, most commonly called main.js. However, this is where the first catch comes in, Electron only supports CommonJS, while Vite only supports ES. One option is to accept that the parts for Electron will be in CommonJS and the parts for Vite/React will be in ES, but this will make it a bit harder to share code. Since Vite prefers ES it is easier to compile the code for Electron from ES to CommonJS. So our entry point will look like this:

main.js

1import { app, BrowserWindow } from 'electron'
2import path from 'path'
3
4const IS_DEV = process.env.IS_IN_DEVELOPMENT || false
5
6function createWindow () {
7  // Create the main Electron window
8  const win = new BrowserWindow({
9    width: 800,
10    height: 600,
11    webPreferences: {
12      nodeIntegration: true,
13      enableRemoteModule: true
14    }
15  })
16
17  if (IS_DEV) {
18    // If we are in development mode we load content from localhost server - vite
19    // and open the developer tools
20    win.loadURL('http://localhost:3000')
21    win.webContents.openDevTools()
22  } else {
23    // In all other cases, load the index.html file from the dist folder
24    win.loadURL(`file://${path.join(__dirname, '..' ,'dist', 'index.html')}`)
25  }
26}
27
28app.whenReady().then(createWindow)
29
30app.on('window-all-closed', () => {
31  // On macOS, it's common for an app and its menu bar to remain
32  // active until the user shuts down the application via the Cmd + Q shortcut
33  if (process.platform !== 'darwin') {
34    app.quit()
35  }
36})
37
38app.on('activate', () => {
39  // On macOS, if an application is in the dock, it is common for a window to be created after
40  // clicking on the icon in the dock if there are no windows active
41  if (BrowserWindow.getAllWindows().length === 0) {
42    createWindow()
43  }
44})

Perhaps the only difference from the one found on the Electron website is the use of ES and the declaration of the global variable IS_DEV, which to distinguish whether the Electron instance should load the web from the Vite server or a generated HTML page in the ./dist folder. Then we need to modify package.json so that the value of "main" refers to our file, i.e. not exactly:

package.json

1{
2  "main": "./dist/main.js",
3}

You may be surprised to know that I am referring to the dist folder, whereas our file is not in any folder. Let me explain.

ESBuild

As I wrote, Electron only supports CommonJS, while Vite and by extension React only works with ES. What about it? The easiest solution is to use some bundler, in this case esbuild, since Vite uses it too. I have also tried Babel, however it conflicts with Vite and the application fails to build. So we install esbuild:

1npm install --save-dev esbuild

Add the following lines to the "scripts" section:

1{
2  "scripts": {
3    "esbuild-cjs": "esbuild main.js --format=cjs --outfile=./dist/main.js",
4    "electron:dev": "npm run esbuild-cjs && IS_IN_DEVELOPMENT=true electron ."
5  }
6}

The first script, esbuild-cjs transforms the EC code into CommonJS and thus makes it digestible for Electron. When we read it carefully, we will also see that the output file is stored in the ./dist/main.js folder, which is where we have referenced in the "main" value of the package.json file. The second script is then used to run the Electron development instance. So, if we have done everything correctly, running npm run electron:dev will first transform the main.js file into CommonJS and only then will Electron be started. If we're running the Vite server at the same time, we'll see the opening page of the React application - identical to the one we saw in the browser after visiting the http://localhost:3000 page. Now all we have to do is choose the right builder and we can develop applications with Vite, React and Electron.

Electron Packager

After the initial fiasco with Electron Builder, I decided to try an alternative for this article, namely Electron Packager. I have to admit that the documentation doesn't compare, Builder has much better documentation, but you should try everything. So let's install Electron Packager:

1npm install --save-dev electron-packager

After installation, just add another script to package.json in the value "scripts", namely:

1{
2  "scripts": {
3    "electron:build": "npm run esbuild-cjs && electron-packager --out=release ."
4  }
5}

The script again first uses an existing esbuild script to convert the main.js file to CommonJS and then calls Electron Builder, which in this case creates an application package for our active paltform. In the case of Windows for Windows, macOS for macOS and Linux for Linux. With that, we're done and all we have to do is play around with the application itself and configure all the components.