Window to Window Communication in Electron

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

Window to Window Communication in Electron

In Electron, it is certainly easy to create a multi-window application app in Electron, but what if you want to communicate between these windows? While we run into a restriction in the Chrome V8 Engine, that requires us to avoid remote code execution, there is a great way to accomplish window-to-window communication in Electron. We can instead create our own message passing system, with the ability to pass any data we want to any window, so it never has to change as the app grows.

Since Electron does not offer a great way to communicate between windows, we will create a system of our own. The system will allow us to communicate between all windows, by passing through the main process.

Before we get to window-to-window communication, we need to first setup the main process to track each window. We do this on the window’s creation, pushing the initialized BrowserWindow object into a shared array, and remove it on window close.

Creating and Tracking Electron Windows

To simplify this, we use a single function to create a window based on three given params:

  • Params Object for the BrowerWindow
  • Window LoadUrl Path
  • Regular Expression for the Window Title

Here’s an example of that function, and using it to create the About Window. For those who want limit individual windows to a single instance, you can see it will also focus the window when it already exists:

windows.js (imported into main.js and menu.js)

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
}

Not only do we have a nice simple way to create and track our windows, but we also have a way to cleanup when we’re done. All that’s needed is to create these Windows somewhere in our electron app.

Since you can only get the current window’s ID, a big issue has been a good way to track down the ID of the destination window. To do this, we initiate lookups using the window title. This gives a solid way to identify windows, especially when the ID can change with each initialization.

Window Message Passing System

The overall concept is to create a centralized system in our Main Process which will act as a router between the windows. We do this by setting up two main process listeners. One for receiving requests, looking up the window and passing data. The second for handling responses and payloads.

The concept is quite simple, but can be a bit of a mind-bender at first glance:

window-to-window communication electron
Here we see how the following system will talk between the main app window and the about window

main.js

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)
})

With the core now setup, we can now setup a secondary window. For example an About renderer process, to request and receive state from the main renderer process.

AboutWindow.js

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}))
}

In the above, the component is set to send requests once the component mounts, sending a request to the message-main-request event in the Main Process, and passes the following params:

  • the about window ID (remote.getCurrentWindow().id)
  • the destination window’s Title (MyApp)
  • the destination window’s receiving event (message-request)

The main process message-main-request listener receives the destination window’s title, and looks up the window with the matching title, then grabs the ID. Using the ID, it sends a new request using the withWindowEvent holding the event string “message-request”. It then uses the requesting window’s ID, and passes any other args from that request.

App.js

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

stateUpdate = () => this.props.state

On the receiving end in the Main Renderer Process, we have an IPC listener for ‘message-request’, which receives the request from the Main Process, and in this example, return the following:

  • event string
  • the origin ID (About Window ID)
  • origin window receiving event string
  • payload data (in this case the local Redux state)

Once it returns the response to the ‘message-request-reply’ listener, the Main Process Listener will start the response. It sends this to the About window’s ‘message-main-response’ IPC Listener, where the payload is passed back. Since we use the spread operator the payload can be anything, how cool!

As you can see, the system itself is fairly simple, and at the same time quite versatile. Because of the spread args you can pass whatever you want between any of your windows, without having to touch the main process listeners.

Window-to-Window Communication

With the main system done, we can now communicate between whatever renderer process listeners you want, as long as they follow the main process listener templates:

Initiating Window Request Template

ipcRenderer.send('message-main-request', <RequestingWindowId>, <DestinationWindowTitle>, <DestinationListenerEvent>, <PayloadArgs>)

Receiving Window Listener Template

  ipcRenderer.on(<DestinationListenerEvent>, (event, <RequestingWindowId>, _type) => event.sender.send('message-request-reply', <RequestingWindowId>, <RequestingWindowListenerEvent>, <Payload>))

In this example we look at requesting data from the app process, but we can request data from the secondary windows in the same way. It doesn’t matter what window you want to get or pass data to, the format is the same.

No matter what the situation, you can now pass data between your windows, without having to duplicate state environments, and without increasing the overhead on your renderer processes. 

If you are still new to Electron, be sure to check out my article on creating Fully-Featured Apps in Electron. It’s covers a lot of the common hang-ups for new-comers to Electron.

No Comments

Add your comment

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