|||

Bridging the Gap: Communicating Between the Browser” (Renderer) and the Main Process in an Electron App

I’ve been working with Electron to create installed desktop applications with JavaScript/TypeScript and I found myself writing up notes about how to have the Browser code communicate with the main” process. I decided to share my notes here in case they’ll be useful for others.

Electron Processes

In Electron there are the following processes to keep in mind:

  • Main: This is what starts your app and orchestrates all the windows. It is a full Node.js process and has access to all Node.js APIs. It also has access to the Electron-specific APIs that simplify dealing with common operating-system-specific features including everything from the clipboard and native system dialogs to the MacOS Touch Bar, and much more.
  • Renderer: Each window that your main app opens gets its own dedicated process that runs the HTML/JavaScript you load into it. This is essentially Chromium and is basically what you’d expect when running code in a browser. Renderer is just a browser, so Renderer does not get access to the OS or Node.js.
  • Preload scripts are really a script run in the renderer process (but not in the browser window’s DOM), but gets more privileges than the renderer process including access to a small number of Node.js modules and globals and some Electron-specific APIs as defined here. The purpose of Preload script is to bridge inter-process communication between the Main and Renderer processes.
  • Utility processes enable creating arbitrary child processes, but I’ll not be delving into those deeply in this post.

The first three are mostly there to mitigate security risks by separating the rendering code running in the browser for more privileged APIs on the user’s local machine.

This part is explained well in Electron’s process model documentation topic, save the note on the Preload script’s purpose and the detail on the sandbox privileges for the Preload script.

Electron Interprocess Communication (IPC) & Terminology

A visual of the interprocess communication is more or less like this:

a sequence diagram of the the interprocess communication in Electrona sequence diagram of the the interprocess communication in Electron

The terminology thoroughly confused me around inter-process communication (IPC).

  • contextBridge.exposeInMainWorld: In this context, main world” is the renderer (not the main” process). 1
  • ipcRenderer is a module that is available only to the renderer process. It allows sending asynchronous messages from a renderer process to the main process.
  • ipcMain is a module that is available only to the main process. It is used to communicate asynchronously from the main process to renderer processes.

Putting it all Together

So putting this together, lets go through a few trivia questions to make sure we’ve got it:

Given the following code in a preload script, which process is ipcRenderer.send(...) sending the message to?

const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})

It is sending to the main process from the renderer process.

Given the following code is in a main script, which process is ipcMain.on(...) listening to messages for?

const { BrowserWindow, ipcMain } = require('electron/main')

ipcMain.on('set-title', (event, title) => {
  const webContents = event.sender
  const win = BrowserWindow.fromWebContents(webContents)
  win.setTitle(title)
})

In this case ipcMain.on will receive messages sent to the main process. So the message sent by the ipcRenderer.send('set-title', title) code from the previous example could be received with the above ipcMain.on('set-title'...) line.

How does the Preload script send a message to the Main process?

Using ipcRender.invoke(...), to receive a response, or ipcRender.send(...) to send a message and not receive a response.

How does the renderer process send a message to the Main process?

The code that you load into a BrowserWindow, cannot require modules such as ipcRenderer that we used above to send a message from a Preload script. However, The preload script can use contextBridge to expose an API to the browser code. The renderer can then use that exposed API to send a message to the preload script and the preload script can forward the messages on to the main process.

sequence diagram showing Renderer sending a message to Mainsequence diagram showing Renderer sending a message to Main

How does the main process send a message to the renderer?

As shown in the Main to render Pattern the main process can access a BrowserWindow.webContents to post a message to the browser similar to the DOMs window.postMessage method used in browser JavaScript.

However, that message is only accessible via the preload script since the message is received by ipcRenderer and the code” that you load into the BrowserWindow cannot require” the ipcRenderer module.

So the preload script requires ipcRenderer to listen for the message sent from the main process, and provides that a method with a callback to the renderer process via contextBridge as shown above and in the linked pattern so that the renderer can indirectly subscribe to the message.

sequence diagram showing Main sending a message to renderersequence diagram showing Main sending a message to renderer

How does the preload script send a message to the Renderer?

You usually don’t? The preload can provide services to the renderer with contextBridge as shown above, but usually it just forwards those up to the main process to handle. However, note that the Main process can send messages to the Renderer as noted above. expensive

Conclusion

Hopefully this will help someone else understand Electron and the process model a bit quicker. If so, share it or leave me a note!

I’ve been experimenting with some ways working with message passing a bit easier. If there is interest I’ll write up some thoughts on that too.

Thanks to Jim Willeke and Imran Khawaja for proofreading and feedback on this article. 🙏


  1. Technically it is just the default renderer process, because you can have other isolated renderers” (called worlds”), but they key point is that it is a renderer process, not a main process.↩︎

Up next Congratulations Micah
Latest posts Bridging the Gap: Communicating Between the “Browser” (Renderer) and the Main Process in an Electron App Congratulations Micah Writing Shell Scripts with TypeScript instead of Bash Surprising Side Effects of Server Side Rendering in Next.js for SEO & Performance When Empowering Employees to Take Risks, Isn’t Empowering (and Why That Needs to Change) Rationalizing Frequent Deployments for Product Managers and Software Engineers Now Write Right: 3 Best Practices for Writing to Innovate and Influence Write Right Now: How Engineers Can Innovate, Influence, and Lead in Business