Developing modern offline apps with ReactJS, Redux and Electron – Part 4 – Electron

No Comments

The previous part of this series showed the beautiful interplay of React and Redux. In this part, we are going to take a rough look at a technology called Electron. One essential technology in our recent projects, Electron is vastly different from the previous two parts of this blog series. React and Redux are solely used to implement the application logic. Electron, on the other hand, is used to implement both structure and application logic to create real cross-platform desktop apps. It is a wrapper which contains a chromium browser in a NodeJS environment. This technology enables the combination of pure web frontend technologies and additionally gives your application full access to the underlying operating system via NodeJS. In the following, we will introduce the basic concepts using a simple Electron app and show how this technology solves the everlasting single-threaded obstacle of non-responsive JavaScript applications.

  1. Introduction
  2. ReactJS
  3. ReactJS + Redux
  4. Electron framework
  5. ES5 vs. ES6 vs. TypeScript
  6. WebPack
  7. Build, test and release process

The Core Parts

An Electron app consists of a few main parts. The basic concept is that you have two or more concurrently running processes. First you have the main process of your application. In this process you have access to NodeJS and thus all your operating system’s power and access to a huge distinct subset of the Electron API. Furthermore the main process creates browser windows. They have one or more render processes and share an important property with your normal browser. These processes are contained in a sandbox. This is because these processes are responsible for rendering the DOM of our web app. Render processes have access to the NodeJS API and a distinct subset of the Electron API, but not to the operating system.

A few functionalities of Electron can even be used in both the main and a render processes. By default JavaScript processes in NodeJS and Chromium are single-threaded and therefore still limited, even if both processes are operating system level processes.

Electron core partsElectron core parts

OS Integration

Since Electron is a JavaScript technology, the final app can be deployed to common desktop operating systems like Windows, MacOS and Linux in 32 and 64-bit versions. To do so, you can use the electron-packager, which is developed by the community. The packager creates installers for various operating systems which make it easy to deploy the Electron apps in enterprise environments. Furthermore, Electron provides essential OS integration on its ownm, menu bars, OS level notifications, file dialogs and many other features for nearly all operating systems.

In our projects we used the file dialog to import files from the file system. The allowed properties depend on the operating system. Please check out the API for more details [DIALOG].

const {dialog} = require('electron');
const properties = ['openFile', 'openDirectory’];
dialog.showOpenDialog({ properties });

We also created custom Electron menu bars for production and development mode. During development we could toggle the developer tools from chromium. For production you can remove that feature from the final Electron app.

 const createMenu = () => {
 const { app, Menu } = electron;
 const template = [
   {
     label: 'Edit',
     submenu: [ 
      { role: 'cut' }, 
      { role: 'copy' }, 
      { role: 'paste' },
      { role: 'selectall' }
    ]
   },
   {
     label: 'View',
     submenu: [ 
      { role: 'reload' },
      { role: 'forcereload' },  
      { role: 'toggledevtools' }
     ]
   }
 ];
 const menu = Menu.buildFromTemplate(template);
 Menu.setApplicationMenu(menu);
};

Electron native menu

To see a full list of all native Electron features, go to [ELECTRON].

IPC Communication

In the previous section we talked about the awesome OS integration of Electron. But how can we harness the full potential of our operating system and backend languages like NodeJS to unleash the power of JavaScript? We can do this with the built-in inter-process-communication in Electron. The modules that handle that communication, the ipcMain and ipcRenderer, are part of Electron’s core. ipcMain enables communication from the main process to the render processes. The ipcRenderer handles the opposite direction from render to main.

“The ipcRenderer module is an instance of the EventEmitter class. It provides a few methods so you can send synchronous and asynchronous messages from the render process (web page) to the main process. You can also receive replies from the main process.” [IPCRENDERER]

In the following example, we register an Event Listener with ipcMain process using the channel name LOAD_FILE_WITH_PATH. Once the Event Listener finishes, we send an event back to the React app. Depending on the result, we add a “success” or “error” to the channel name. This allows us to operate differently with the response inside React [IPCMAIN].

In the React app, we use the ipcRenderer.send to send messages asynchronously to the Event Listener, using the identical channel name. To send messages synchronously use ipcRenderer.sendSync. After that we add a one time listener function for the event using ipc.once. To distinguish IPC calls we add a unique uuid to the Channel name [IPCRENDERER].

electron.js
const ipc = require('electron').ipcMain;
ipc.on(ipcConstants.LOAD_FILE_WITH_PATH, async (event, request) => {
  try {
    const fileContent = await fileService.readFileAsync(request.path);
    event.sender.send(
      `${ipcConstants.LOAD_FILE_WITH_PATH}-success-${request.uuid}`, fileContent);
  } catch (error) {
    event.sender.send(
      `${ipcConstants.LOAD_FILE_WITH_PATH}-error-${request.uuid}`, error.message);
  }
});
fileService.js
const ipc = require('electron').ipcRenderer;
export function readFileContentFromFileSystem(path) {
  const uuid = uuidV4();
  ipc.send(LOAD_FILE_WITH_PATH, { uuid, path });
  return new Promise((resolve, reject) => {
    ipc.once(`${LOAD_FILE_WITH_PATH}-success-${uuid}`,
      (event, xml) => {
        resolve(xml);
      });
    ipc.once(`${LOAD_FILE_WITH_PATH}-error-${uuid}`,
      (event, args) => {
        reject(args);
      });
  });
}

To debug the IPC communication between your React application and Electron, you need to install the Electron DevTools Extension.

npm install --save-dev devtron

Afterwards run the following command from the console tab of your application. This will add another tab with the Devtron tools.

require('devtron').install()

Under the Devtron tab you get all kinds of details about your Electron application. Devtron displays all default event listeners from Electron as well as your own custom listeners. Under the IPC link you can record all IPC calls from your application. The Lint tab allows you to do Lint checks and the Accessibility tab checks your web application against the Accessible Rich Internet Applications Suite (ARIA) standard.

Devtron event listener

Here is an example what the IPC communication in our project looks like.

Devtron IPC call

Remember that we claimed that Electron is the end of the everlasting single-threaded obstacle? Using IPC we can move CPU intensive work to Electron and outsource these tasks using electron-remote. With one single line we can create a task pool that will actually create a new browser window in the background and execute our code (electronFileService.js) in a separate OS process / browser window. Here is an example how to setup the task pool for the file service.

const { requireTaskPool } = require('electron-remote');
const fileService = requireTaskPool(require.resolve('./electronFileService'));

Offline and Storage

When developing an offline desktop application with Electron you have several options on where to store and read data from.

Option 1: Electron / NodeJS

In Electron you can execute NodeJS commands. Therefore you can use almost any module from npmjs.org to read and store data on your local operating system. We recommend this option when you need to persist and process a lot of data.

  • SQLite3 (relational database)[SQLITE]
  • MongoDB (document database)[MONGODB]
  • Neo4J (graph database)[NEO4J]

Electron app

Option 2: React & Redux / Web Browser

In the second option we persist and process data inside the browser. Modern browsers offer a range of APIs that allow for persisting browser data, i.e. LocalStorage, IndexedDB, SessionStorage, WebSQL and Cookies. We recommend this approach for small datasets that need to be persisted locally. This can be done with any web technology. In our case, the React web application uses Redux as a store for the application state. You can use the redux-persist module to automatically persist the Redux store to the IndexedDB or LocalStorage. In case your web app crashes or you restart the browser, you can configure redux-persist [REDUXP] to automatically rehydrate the Redux Store.

React WebApp

Modern browsers support service worker API to span threads for processing data. If there is information that you need to persist and reuse across restarts, service workers have access to the various browser storage technologies.

Option 3: Combination of Option 1 and 2

There might be times when your desktop client will be online and can retrieve data from a backend server. With our proposed stack you have the full freedom of choosing how to access the backend services. You can either call the backend services via the web application layer (i.e. React WebApp) or you can use the Electron/NodeJS layer. Which way you choose is up to you and might depend on security restrictions or the existence of NodeJS modules you can reuse or other aspects.

Electron React App

Summary

Electron is an extremely powerful technology that enables you and your team to create beautiful, responsive, OS independent and maintainable desktop applications. Because there is so much more to Electron, we highly recommend reading https://electronjs.org/docs for the parts that you are interested in or need in your projects. Just keep tuned for our next article.

References

More content about JavaScript

Comment

Your email address will not be published. Required fields are marked *