IPC channels are arbitrary and bidirectional
In Electron, IPC channels can be named anything you want (arbitrary names) and can be used for communication in both directions (bidirectional). You can use the same channel name for both ipcMain and ipcRenderer modules.
Pattern 1: Renderer to main one-way IPC
Use ipcRenderer.send from the renderer process to send a one-way message to the main process, and ipcMain.on to listen for it. This pattern is typically used to call a main process API from web contents. The ipcMain.on listener receives an IpcMainEvent structure and any passed arguments.
Pattern 2: Renderer to main two-way IPC with invoke
Use ipcRenderer.invoke from the renderer process to call a function in the main process and wait for a result. In the main process, use ipcMain.handle to listen for the invocation and return a value. The return value is returned as a Promise to the original invoke call.
IPC channel naming convention with colon prefix
Using a colon prefix in channel names (e.g., 'dialog:openFile') has no effect on code execution. It serves only as a namespace convention to help with code readability and organization.
Pattern 2 example: two-way IPC with invoke
Example showing two-way IPC pattern using ipcRenderer.invoke and ipcMain.handle:
Main process (main.js):
```js
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('node:path')
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
```
Preload script (preload.js):
```js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
```
Renderer process (renderer.js):
```js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
```
Pattern 1 example: one-way IPC with send
Example showing one-way IPC pattern using ipcRenderer.send and ipcMain.on:
Main process (main.js):
```js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle)
createWindow()
})
```
Preload script (preload.js):
```js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
```
Renderer process (renderer.js):
```js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
```
Pattern 3: Main to renderer IPC
To send a message from the main process to a renderer process, use the WebContents instance's send method. You must specify which renderer is receiving the message by targeting its WebContents instance. The send method on WebContents works the same way as ipcRenderer.send.
Pattern 3 example: main to renderer with webContents.send
Example showing main-to-renderer IPC pattern using webContents.send and ipcRenderer.on:
Main process (main.js):
```js
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment'
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement'
}
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
}
```
Preload script (preload.js):
```js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
```
Renderer process (renderer.js):
```js
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})
```
Legacy: two-way IPC with ipcRenderer.send and event.reply
Prior to Electron 7, two-way IPC was performed using ipcRenderer.send paired with event.reply in the main process. This approach requires setting up a separate ipcRenderer.on listener to handle the response. It's no longer recommended; use ipcRenderer.invoke instead.
Legacy: ipcRenderer.sendSync blocks renderer process
The ipcRenderer.sendSync API sends a message to the main process and waits synchronously for a response. This blocks the renderer process until a reply is received and should be avoided for performance reasons. Use ipcRenderer.invoke instead.
Pattern 4: Renderer to renderer communication
There is no direct way to send messages between renderer processes using ipcMain and ipcRenderer. To achieve renderer-to-renderer communication, use one of two options: (1) use the main process as a message broker by forwarding messages between renderers, or (2) pass a MessagePort from the main process to both renderers to enable direct communication after initial setup.
IPC serialization uses Structured Clone Algorithm
Electron's IPC implementation uses the HTML standard Structured Clone Algorithm to serialize objects passed between processes. Only certain types of objects can be passed through IPC channels. DOM objects (e.g., Element, Location, DOMMatrix), Node.js objects backed by C++ classes (e.g., process.env, some Stream members), and Electron objects backed by C++ classes (e.g., WebContents, BrowserWindow, WebFrame) are not serializable with Structured Clone.
Main to renderer reply pattern
To send a reply from renderer back to main process after receiving a main-to-renderer message, expose an additional API through the context bridge that calls ipcRenderer.send. The main process can then listen for this reply using ipcMain.on.
MessagePort basics: creation and channel pairs
MessagePorts are created in pairs using new MessageChannel(). A connected pair of message ports is called a channel. Messages sent to port1 will be received by port2 and vice-versa. Messages sent before the other end has registered a listener will be queued until a listener is registered.
Transferring MessagePorts between processes
MessagePorts can be sent between the renderer and main process using ipcRenderer.postMessage() and WebContents.postMessage() methods. The usual IPC methods like send and invoke cannot be used to transfer MessagePorts; only the postMessage methods can transfer MessagePorts. MessagePorts can also be sent to other frames or Web Workers.
MessagePortMain in main process
The main process does not have the web-standard MessagePort or MessageChannel classes because it is not a web page and has no Blink integration. Instead, Electron provides MessagePortMain and MessageChannelMain classes. When a MessagePort is received in the main process via IPC, it becomes a MessagePortMain.
MessagePortMain events API uses Node.js style
MessagePortMain uses the Node.js-style events API rather than the web-style events API. Use .on('message', ...) instead of .onmessage = ... when working with MessagePortMain in the main process.
MessagePortMain requires start() to receive queued messages
MessagePortMain queues messages until the .start() method has been called. Messages will be held until start() is invoked.
MessagePort close event extension in Electron
Electron adds a close event to MessagePort that is not present on the web. The close event is emitted when the other end of the channel is closed. Ports can also be implicitly closed by being garbage-collected. In the renderer, listen for close using port.onclose or port.addEventListener('close', ...). In the main process, listen using port.on('close', ...).
Direct renderer-to-renderer communication via MessagePorts
The main process can set up a MessageChannel and send each port to a different renderer window using webContents.postMessage(). This allows two renderers to send messages to each other directly without using the main process as an intermediary.
Worker process pattern with MessagePorts
MessagePorts enable a worker process pattern where a hidden BrowserWindow acts as a worker. The main process creates a MessageChannel, sends one port to the worker and another to the main window, allowing direct communication between them without relaying messages through the main process. This reduces performance overhead.
Implementing response streams with MessagePorts
Electron's built-in IPC methods only support fire-and-forget (send) or request-response (invoke) modes. Using MessagePorts, you can implement a response stream where a single request responds with multiple messages before closing the port to signal completion.
MessagePort bypasses same-origin restrictions
By passing MessagePorts via the main process, you can connect two pages that might not otherwise be able to communicate due to same-origin restrictions.
Example: MessageChannel between two renderers
In main.js: Create MessageChannelMain, send port1 to mainWindow using webContents.postMessage() on 'ready-to-show', and port2 to secondaryWindow on its 'ready-to-show' event. In preload scripts: Use ipcRenderer.on('port', e => { window.electronMessagePort = e.ports[0]; window.electronMessagePort.onmessage = ... }) to receive the port. In renderer: Call window.electronMessagePort.postMessage() to send messages to the other renderer.
Example: Response stream implementation with MessagePorts
Renderer creates new MessageChannel(), sends port2 to main with ipcRenderer.postMessage('give-me-a-stream', data, [port2]), keeps port1, and listens to port1.onmessage and port1.onclose. Main process receives port via ipcMain.on(), sends multiple messages via replyPort.postMessage(), and closes with replyPort.close() when done.
Receiver frame postMessage for MessagePorts
MessagePorts can be transferred using event.senderFrame.postMessage() to send a port directly to a specific frame that sent an IPC message, enabling direct communication between that frame and another process.
Node-API threadsafe function for Swift callbacks
Use napi_create_threadsafe_function in the Node.js addon to safely handle callbacks from Swift running on different threads. Create threadsafe functions with napi_create_threadsafe_function, passing a callback that executes on the Node.js event loop. Store Swift callbacks as Objective-C blocks that call napi_call_threadsafe_function to bridge thread boundaries. Release with napi_release_threadsafe_function in the destructor.
Preload script setup for drag and drop IPC
In preload.js, use contextBridge to expose a method window.electron.startDrag() that sends an IPC message to the main process. Example: contextBridge.exposeInMainWorld('electron', { startDrag: (fileName) => ipcRenderer.send('ondragstart', fileName) })
Main process drag and drop with file path and icon
In main.js, listen for the 'ondragstart' IPC message and call event.sender.startDrag() with an object containing 'file' (full path to the file being dragged) and 'icon' (path to the icon image).
Use IPC to access renderer APIs from main process or vice versa
If you need to use a renderer process API in the main process or vice versa, consider using inter-process communication (IPC).
Preload scripts execute before web content loading in renderer
Preload scripts contain code that executes in a renderer process before its web content begins loading. These scripts run within the renderer context but are granted more privileges by having access to Node.js APIs.
Attach preload script in BrowserWindow constructor webPreferences
A preload script is attached to the main process using the BrowserWindow constructor's webPreferences option.
BrowserWindow with preload script example
Example code showing how to attach a preload script:
```js
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
```
Preload script shares Window global with renderer
Preload scripts share a global Window interface with the renderers and can access Node.js APIs. They serve to enhance the renderer by exposing arbitrary APIs in the window global that web contents can consume.
Use contextBridge to securely expose APIs from preload to renderer
Instead of directly attaching to window, use the contextBridge module to expose APIs from the preload script to the renderer. This accomplishes the goal securely while respecting context isolation.
contextBridge.exposeInMainWorld example
Example showing secure API exposure using contextBridge:
preload.js:
```js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
```
renderer.js:
```js
console.log(window.myAPI)
// => { desktop: true }
```
Preload script use case: expose ipcRenderer for IPC
Preload scripts are useful for exposing ipcRenderer helpers to the renderer, allowing use of inter-process communication (IPC) to trigger main process tasks from the renderer and vice-versa.
Preload script use case: add desktop-only logic to remote web app
If developing an Electron wrapper for an existing web app hosted on a remote URL, you can use preload scripts to add custom properties onto the renderer's window global that can be used for desktop-only logic on the web client's side.
IPC using ipcRenderer.invoke and ipcMain.handle
To communicate between renderer and main processes, you can set up a main process handler with ipcMain.handle and expose a function in the preload script that calls ipcRenderer.invoke to trigger the handler. The handler is triggered from the renderer via the defined channel.
IPC security - never expose ipcRenderer module directly
You must wrap ipcRenderer.invoke calls in helper functions in preload scripts rather than expose the ipcRenderer module directly via context bridge. Directly exposing the entire ipcRenderer module would give the renderer the ability to send arbitrary IPC messages to the main process, which becomes a powerful attack vector for malicious code.
Example preload script exposing ping function via IPC
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping')
// we can also expose variables, not just functions
})
This example shows how to securely expose an IPC invoke call by wrapping it in a helper function exposed via contextBridge.
Example main process setting up IPC handler
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong')
createWindow()
})
This example shows setting up an ipcMain.handle listener before loading the HTML file so the handler is ready before the renderer sends an invoke call.
Example renderer calling exposed IPC function
const func = async () => {
const response = await window.versions.ping()
console.log(response) // prints out 'pong'
}
func()
This example shows how to call an IPC function exposed in the preload script from the renderer process.