Electron is an open-source framework developed and maintained by GitHub. It is a great tool for building cross-platform desktop applications using web technologies. Electron couples Chromium’s rendering engine with the Node.js runtime environment, which results in a potent combination. This provides the ability to use web technologies such as HTML, CSS and JavaScript (including frameworks such as React) while leveraging Node.js API’s to perform lower-level OS functions such as file I/O. What is Electron’s IPC module and why do we need it?
Electron uses Chromium and with it, Chromium’s multi-process architecture. Every web page that is created in Electron runs in its own process, similar to the tabs in Google Chrome. Electron has two types of processes. First, we have the main process which is used to handle application lifecycle events, native file menus and the management of renderer processes. The second type of process is called a renderer. These processes are responsible for loading web pages. They can be created from the main process using Electron’s BrowserWindow module. Electron apps contain only one main process but can have multiple renderer processes. For example, an Electron application with two BrowserWindows will spawn a separate process for each instance.
While working with Electron, it is likely that you will need to communicate between processes. Electron provides us with two modules to achieve this: ipcMain and ipcRenderer. In this guide, we will use both of these modules to communicate a native file menu action from the main process to a renderer process and send a reply back to the main process.
To get started, clone this Electron/React boilerplate project.
First, we’ll import the Menu object from electron in the main process. Menu should be added to the import statement on line 12 of main.dev.ts:
import { app, BrowserWindow, Menu } from 'electron';
Next, we’ll add a function that will communicate an action from the main process to the renderer process via an IPC channel when “Action” is selected from the file menu:
const sendMessage = () => {
if (mainWindow) {
mainWindow.webContents.send('my-ipc-channel', {
message: 'Message communicated from the main process!'
});
}
};
After that, we’ll create a basic file menu with a single action. Replace lines 91 and 92 in main.dev.ts with the following snippet:
/**
* Native File Menu
*/
const menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'Action',
click: sendMessage,
accelerator: 'CmdOrCtrl+A'
}
]
}
];
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
Next, we’ll need to subscribe to the same channel in our renderer process. In the components directory, create a file called ipcExample.tsx and paste the following code:
import React, { useEffect, useState } from 'react';
const { ipcRenderer } = window.require('electron');
interface Response {
[message: string]: string;
}
export default function IpcExample() {
const [message, setMessage] = useState('');
const handleResponse = (_event: any, response: Response) => {
setMessage(response.message);
};
useEffect((): any => {
ipcRenderer.on('my-ipc-channel', handleResponse);
return () => ipcRenderer.off('my-ipc-channel', handleResponse);
}, []);
return <>{message}</>;
}
Here we are subscribing to an IPC channel when our react component is mounted. In our useEffect’s cleanup function, we are unsubscribing to the channel to avoid creating duplicate subscriptions. The handleResponse function is setting a state which is displayed on the DOM when an IPC message is received via the ‘’my-ipc-example’ channel.
Finally, import the IpcExample component in Home.tsx. Home.tsx should now look like this:
import React from 'react';
import { Link } from 'react-router-dom';
import routes from '../constants/routes.json';
import styles from './Home.css';
import IpcExample from './ipcExample';
export default function Home() {
return (
<div className={styles.container} data-tid="container">
<div>
<IpcExample />
</div>
<Link to={routes.COUNTER}>to Counter</Link>
</div>
);
}
We’ve now opened a line of communication from the main to the renderer process, but what if we wanted to send a response back to the main process? We can achieve this using the event provided to us as the first parameter in the handleResponse() function. We will send a reply from the renderer process back to the main process via a new IPC channel.
First, we’ll update the handleResponse() function in the renderer process to send the reply:
const handleResponse = (_event: any, response: Response) => {
setMessage(response.message);
_event.sender.send('ipc-reply', { success: true });
};
Next, we’ll create a listener in our main process (main.dev.ts) to receive the reply using the ipcMain module. Update the import to include the ipcMain module:
import { app, BrowserWindow, Menu, ipcMain } from 'electron';
After that, we’ll add the listener. This can be placed at the very end of main.dev.ts:
ipcMain.on('ipc-reply', (_event, payload) => {
const result = payload.success ? 'Success!' : 'Failure';
console.log(result);
});
You should now see “Success” printed out in the console where the main process was launched from when “Action” is selected from the file menu.
That’s it! We have now communicated a file menu click from the main to the renderer process and sent a reply back. The IPC module is a powerful and necessary tool when navigating Electron’s multi-process architecture. The ability to communicate between processes opens the door for many use cases such as persisting state across multiple windows and managing window states.
More information on IPC functionality can be found here: