Window to Window Messaging with @electron/remote

We create open-source because we love it, and we share our finding so everyone else can benefit as well.

electron ipc system

Window to Window Messaging with @electron/remote

In a previous article, I covered how to accomplish Window to Window Communication in Electron with a message passing system. Since Electron 14 removed the remote module, this system will not work in older versions. So in this article we will discuss how to upgrade this window to window messaging system to use @electron/remote.

To keep this article short, I will only talk about the system briefly, so if you haven’t read the previous article, please check out Window to Window Communication in Electron.

Getting Started with @electron/remote

So previously we created a message system in Electron’s main process to handle, or broker messages from our different windows, returning replies to the windows requesting data. Since Electron has removed the needed remote module, we’ll be installing and configuring the app to use @electron/remote. Like anything else, we first need to install our module:

$ yarn add @electron/remote

Once installed we need to update Electron’s main file to use the new remote module. First add the module import after your other imports, and call the initialize method of the module. This will enable the remote module on our electron objects.

require('@electron/remote/main').initialize()

After we create our BrowserWindow in the main file, we also want to enable webContents for our windows. We can do this with a listener on our app object, which should be placed near our other app listeners (‘ready’, ‘activate’, etc).

app.on('browser-window-created', (_, window) => {
  require('@electron/remote/main').enable(window.webContents)
})

Let’s now move on to our messaging system.

Window to Window Messaging

Secondary Window Function

So some of the biggest hurdles come from updating the way secondary windows need to be updated. We lose some functionality, but we do have other options to replace it. Here’s the code for the previous versions of Electron.

// windows.js Electron < 14.x
const windowList = [] // shared array

function createWindow(windowOptions, windowPath, winRegex) {
  if(!windowList.filter(winItem => winItem.webContents.browserWindowOptions.title && winRegex.test(winItem.webContents.browserWindowOptions.title)).length) 
{
    let win = new BrowserWindow(Object.assign(windowOptions))
    win.loadURL(windowPath)
    windowList.push(win)

    win.on('close', () => {
      // remove window when it is closed
      windowList.splice(windowList.indexOf(win), 1)
    })
  } else {
    // if window already open, focus
    windowList.filter(winItem => winItem.webContents.browserWindowOptions.title && winRegex.test(winItem.webContents.browserWindowOptions.title))[0].focus()
  }
}

const aboutPath = process.env.NODE_ENV === 'development' ? path.join('file://', process.cwd(), 'src', 'about.html') : path.join('file://', __dirname, 'about.html')

const aboutRegex = /^About MyApp$/

// example use
createWindow({
  title: 'About MyApp',
  width: 300,
  height: process.platform !== 'darwin' ? 330 : 300,
  maxHeight: 330,
  maxWidth: 300,
  minWidth: 300,
  minHeight: 300,
  frame: process.platform !== 'darwin' ? true : false,
  webPreferences: {
    devTools: process.env.NODE_ENV === 'development',
    nodeIntegration: true
  }
}, aboutPath, aboutRegex);

module.exports = {
  createWindow: createWindow
  windowList: windowList
}

To check if a window exists, we check if the property exists, and then compare with the webContents title. When the window closes, we use this same method to find the correct window to remove. Lastly, we need to add a line which will add the webContents object to our windows. So to fix this, we refactor to use the getTitle function.

// windows.js Electron > 14.x with @electron/remote
const windowList = [] // shared array

function createWindow(windowOptions, windowPath, winRegex) {
  if(!windowList.filter(winItem => winRegex.test(winItem.getTitle())).length) {
    let win = new BrowserWindow(Object.assign(windowOptions))
    win.loadURL(windowPath)
    windowList.push(win)

    win.on('close', () => {
      // remove window when it is closed
      windowList.splice(windowList.indexOf(win), 1)
    })

    require('@electron/remote/main').enable(window.webContents)
  } else {
    // if window already open, focus
    windowList.filter(winItem => winRegex.test(winItem.getTitle()))[0].focus()
  }
}

...

With these lines replaced, we can start the app, and at least test that our windows open correctly.

Main Process Message Broker

To fix the code for our Window to Window Communication, we now need to update our IPC listeners in our main process. Let’s quickly review the older code:

// main.js Electron < 14.x
...

ipcMain.on('message-main-request', (event, fromWindowId, toWindowTitle, withWindowEvent, ...args) => {
  let toWindowId = windowList.filter(w => w.webContents.browserWindowOptions.title === toWindowTitle)[0].id
  let browserWindow = BrowserWindow.fromId(toWindowId)
  browserWindow.webContents.send(withWindowEvent, fromWindowId, ...args)
});

ipcMain.on('message-request-reply', (event, fromWindowId, withWindowEvent, ...args) => {
  let browserWindow = BrowserWindow.fromId(fromWindowId)
  browserWindow.webContents.send(withWindowEvent, fromWindowId, ...args)
})

...

Both of these listeners listen for messages from the render process, and send a reply to the window defined by the window sending the first message. First we run into the same issue, where we’re trying to read the title from the WindowOptions, which we will replace with getTitle again. Next we need to change the listener methods used by ipcMain, and use the newer methods.

// main.js Electron > 14.x with @electron/remote
...

ipcMain.handle('message-main-request', (event, fromWindowId, toWindowTitle, withWindowEvent, ...args) => {
  let toWindowId = windowList.filter(w => w.getTitle() === toWindowTitle)[0].id
  let browserWindow = BrowserWindow.fromId(toWindowId)
  browserWindow.webContents.send(withWindowEvent, fromWindowId, ...args)
});

ipcMain.handle('message-request-reply', (event, fromWindowId, withWindowEvent, ...args) => {
  let browserWindow = BrowserWindow.fromId(fromWindowId)
  browserWindow.webContents.send(withWindowEvent, fromWindowId, ...args)
})

As you can see, we use the getTitle function for our condition to find the correct window, and then we set ipcMain to use the handle eventListener.

Renderer Process Messages

Let now finish up with our renderer process messengers. In the AboutWindow file we were using a message-request call to send a message request to another window, and then a message-response listener for messages sent to that window relayed through the main process.

// AboutWindow.js Electron < 14.x

import { remote, ipcRenderer } from 'electron' 

constructor(props) {
  super(props)

  this.current = remote.getCurrentWindow()
}

componentDidMount() {
  this.interval = setInterval(() => {
    ipcRenderer.send('message-main-request', this.current.id, 'MyApp', 'message-request')
  }, 2000)

  ipcRenderer.on('message-main-response', (event, windowId, reduxData) => this.setState({reduxState: reduxData}))
}

Before we fix anything else, we need to update the module used for our remote module, switching to @electron/remote. Next, we no longer want to use the send function, so we’ll replace that with the new invoke function. Everything else can stay the same.

Note: The @electron/remote import must use ES5 syntax

// AboutWindow.js Electron > 14.x with @electron/remote

import { ipcRenderer } from 'electron'
const remote = require('@electron/remote')

constructor(props) {
  super(props)

  this.current = remote.getCurrentWindow()
}

componentDidMount() {
  this.interval = setInterval(() => {
    ipcRenderer.invoke('message-main-request', this.current.id, 'MyApp', 'message-request')
  }, 2000)

  ipcRenderer.on('message-main-response', (event, windowId, reduxData) => this.setState({reduxState: reduxData}))
}

Moving on to our App.js file, we listen for a message-request event passed through the main process, and the use the message-request-reply to return the reply to main.

// App.js Electron < 14.x

componentDidMount() {
  ipcRenderer.on('message-request', (event, fromWindowId, _type) => event.sender.send('message-request-reply', fromWindowId, 'message-main-response', this.stateUpdate()))
}

stateUpdate = () => this.props.state

With this file, we only need to replace the old event reply method, and then our message system is functional again.

// App.js Electron > 14.x with @electron/remote

componentDidMount() {
  ipcRenderer.on('message-request', (event, fromWindowId, _type) => ipcRenderer.invoke('message-request-reply', fromWindowId, 'message-main-response', this.stateUpdate()))
}

stateUpdate = () => this.props.state

Summary

That’s all that’s needed to replace remote in our Window-to-Window Communication system with @electron/remote. Everything works the same, but a few different corner-cases can keep the transition from working flawlessly, especially the webContents enabling.

 

No Comments

Add your comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.