diff --git a/docs/api/ipc-main.md b/docs/api/ipc-main.md index f55e848c16398..e7c9800a25b50 100644 --- a/docs/api/ipc-main.md +++ b/docs/api/ipc-main.md @@ -1,3 +1,10 @@ +--- +title: "ipcMain" +description: "Communicate asynchronously from the main process to renderer processes." +slug: ipc-main +hide_title: false +--- + # ipcMain > Communicate asynchronously from the main process to renderer processes. @@ -9,7 +16,9 @@ process, it handles asynchronous and synchronous messages sent from a renderer process (web page). Messages sent from a renderer will be emitted to this module. -## Sending Messages +For usage examples, check out the [IPC tutorial]. + +## Sending messages It is also possible to send messages from the main process to the renderer process, see [webContents.send][web-contents-send] for more information. @@ -21,36 +30,6 @@ process, see [webContents.send][web-contents-send] for more information. coming from frames that aren't the main frame (e.g. iframes) whereas `event.sender.send(...)` will always send to the main frame. -An example of sending and handling messages between the render and main -processes: - -```javascript -// In main process. -const { ipcMain } = require('electron') -ipcMain.on('asynchronous-message', (event, arg) => { - console.log(arg) // prints "ping" - event.reply('asynchronous-reply', 'pong') -}) - -ipcMain.on('synchronous-message', (event, arg) => { - console.log(arg) // prints "ping" - event.returnValue = 'pong' -}) -``` - -```javascript -// In renderer process (web page). -// NB. Electron APIs are only accessible from preload, unless contextIsolation is disabled. -// See https://www.electronjs.org/docs/tutorial/process-model#preload-scripts for more details. -const { ipcRenderer } = require('electron') -console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong" - -ipcRenderer.on('asynchronous-reply', (event, arg) => { - console.log(arg) // prints "pong" -}) -ipcRenderer.send('asynchronous-message', 'ping') -``` - ## Methods The `ipcMain` module has the following method to listen for events: @@ -59,7 +38,7 @@ The `ipcMain` module has the following method to listen for events: * `channel` string * `listener` Function - * `event` IpcMainEvent + * `event` [IpcMainEvent][ipc-main-event] * `...args` any[] Listens to `channel`, when a new message arrives `listener` would be called with @@ -69,7 +48,7 @@ Listens to `channel`, when a new message arrives `listener` would be called with * `channel` string * `listener` Function - * `event` IpcMainEvent + * `event` [IpcMainEvent][ipc-main-event] * `...args` any[] Adds a one time `listener` function for the event. This `listener` is invoked @@ -93,8 +72,8 @@ Removes listeners of the specified `channel`. ### `ipcMain.handle(channel, listener)` * `channel` string -* `listener` Function | any> - * `event` IpcMainInvokeEvent +* `listener` Function { const result = await somePromise(...args) return result }) +``` -// Renderer process +```js title='Renderer Process' async () => { const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2) // ... @@ -130,7 +109,7 @@ provided to the renderer process. Please refer to ### `ipcMain.handleOnce(channel, listener)` * `channel` string -* `listener` Function | any> +* `listener` Function Communicate asynchronously from a renderer process to the main process. @@ -9,7 +16,7 @@ methods so you can send synchronous and asynchronous messages from the render process (web page) to the main process. You can also receive replies from the main process. -See [ipcMain](ipc-main.md) for code examples. +See [IPC tutorial](../tutorial/ipc.md) for code examples. ## Methods @@ -70,7 +77,7 @@ throw an exception. > them. Attempting to send such objects over IPC will result in an error. The main process handles it by listening for `channel` with the -[`ipcMain`](ipc-main.md) module. +[`ipcMain`](./ipc-main.md) module. If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer). @@ -98,7 +105,7 @@ throw an exception. > them. Attempting to send such objects over IPC will result in an error. The main process should listen for `channel` with -[`ipcMain.handle()`](ipc-main.md#ipcmainhandlechannel-listener). +[`ipcMain.handle()`](./ipc-main.md#ipcmainhandlechannel-listener). For example: @@ -124,11 +131,11 @@ If you do not need a response to the message, consider using [`ipcRenderer.send` * `channel` string * `...args` any[] -Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler. +Returns `any` - The value sent back by the [`ipcMain`](./ipc-main.md) handler. Send a message to the main process via `channel` and expect a result synchronously. Arguments will be serialized with the [Structured Clone -Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be +Algorithm][SCA], just like [`window.postMessage`], so prototype chains will not be included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. @@ -140,13 +147,13 @@ throw an exception. > Electron's IPC to the main process, as the main process would have no way to decode > them. Attempting to send such objects over IPC will result in an error. -The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module, +The main process handles it by listening for `channel` with [`ipcMain`](./ipc-main.md) module, and replies by setting `event.returnValue`. > :warning: **WARNING**: Sending a synchronous message will block the whole > renderer process until the reply is received, so use this method only as a > last resort. It's much better to use the asynchronous version, -> [`invoke()`](ipc-renderer.md#ipcrendererinvokechannel-args). +> [`invoke()`](./ipc-renderer.md#ipcrendererinvokechannel-args). ### `ipcRenderer.postMessage(channel, message, [transfer])` @@ -158,7 +165,7 @@ Send a message to the main process, optionally transferring ownership of zero or more [`MessagePort`][] objects. The transferred `MessagePort` objects will be available in the main process as -[`MessagePortMain`](message-port-main.md) objects by accessing the `ports` +[`MessagePortMain`](./message-port-main.md) objects by accessing the `ports` property of the emitted event. For example: @@ -197,7 +204,7 @@ the host page instead of the main process. ## Event object The documentation for the `event` object passed to the `callback` can be found -in the [`ipc-renderer-event`](structures/ipc-renderer-event.md) structure docs. +in the [`ipc-renderer-event`](./structures/ipc-renderer-event.md) structure docs. [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter [SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm diff --git a/docs/fiddles/communication/two-processes/.keep b/docs/fiddles/communication/two-processes/.keep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/index.html b/docs/fiddles/communication/two-processes/asynchronous-messages/index.html deleted file mode 100644 index 43d23a29087f2..0000000000000 --- a/docs/fiddles/communication/two-processes/asynchronous-messages/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -
-
-

Asynchronous messages

- Supports: Win, macOS, Linux | Process: Both -
-
- - -
-

Using ipc to send messages between processes asynchronously is the preferred method since it will return when finished without blocking other operations in the same process.

- -

This example sends a "ping" from this process (renderer) to the main process. The main process then replies with "pong".

-
-
-
- - - diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/main.js b/docs/fiddles/communication/two-processes/asynchronous-messages/main.js deleted file mode 100644 index 942f7022f8590..0000000000000 --- a/docs/fiddles/communication/two-processes/asynchronous-messages/main.js +++ /dev/null @@ -1,29 +0,0 @@ -const { app, BrowserWindow, ipcMain } = require('electron') - -let mainWindow = null - -function createWindow () { - const windowOptions = { - width: 600, - height: 400, - title: 'Asynchronous messages', - webPreferences: { - nodeIntegration: true - } - } - - mainWindow = new BrowserWindow(windowOptions) - mainWindow.loadFile('index.html') - - mainWindow.on('closed', () => { - mainWindow = null - }) -} - -app.whenReady().then(() => { - createWindow() -}) - -ipcMain.on('asynchronous-message', (event, arg) => { - event.sender.send('asynchronous-reply', 'pong') -}) diff --git a/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js b/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js deleted file mode 100644 index 40ed596201ad2..0000000000000 --- a/docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js +++ /dev/null @@ -1,12 +0,0 @@ -const { ipcRenderer } = require('electron') - -const asyncMsgBtn = document.getElementById('async-msg') - -asyncMsgBtn.addEventListener('click', () => { - ipcRenderer.send('asynchronous-message', 'ping') -}) - -ipcRenderer.on('asynchronous-reply', (event, arg) => { - const message = `Asynchronous message reply: ${arg}` - document.getElementById('async-reply').innerHTML = message -}) diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/index.html b/docs/fiddles/communication/two-processes/synchronous-messages/index.html deleted file mode 100644 index 055fcf3473ce1..0000000000000 --- a/docs/fiddles/communication/two-processes/synchronous-messages/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -
-
-

Synchronous messages

- Supports: Win, macOS, Linux | Process: Both -
-
- - -
-

You can use the ipc module to send synchronous messages between processes as well, but note that the synchronous nature of this method means that it will block other operations while completing its task.

- -

This example sends a synchronous message, "ping", from this process (renderer) to the main process. The main process then replies with "pong".

-
-
-
- - - \ No newline at end of file diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/main.js b/docs/fiddles/communication/two-processes/synchronous-messages/main.js deleted file mode 100644 index 1adb7c02c9f11..0000000000000 --- a/docs/fiddles/communication/two-processes/synchronous-messages/main.js +++ /dev/null @@ -1,29 +0,0 @@ -const { app, BrowserWindow, ipcMain } = require('electron') - -let mainWindow = null - -function createWindow () { - const windowOptions = { - width: 600, - height: 400, - title: 'Synchronous Messages', - webPreferences: { - nodeIntegration: true - } - } - - mainWindow = new BrowserWindow(windowOptions) - mainWindow.loadFile('index.html') - - mainWindow.on('closed', () => { - mainWindow = null - }) -} - -app.whenReady().then(() => { - createWindow() -}) - -ipcMain.on('synchronous-message', (event, arg) => { - event.returnValue = 'pong' -}) \ No newline at end of file diff --git a/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js b/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js deleted file mode 100644 index 4769b6f97f714..0000000000000 --- a/docs/fiddles/communication/two-processes/synchronous-messages/renderer.js +++ /dev/null @@ -1,9 +0,0 @@ -const { ipcRenderer } = require('electron') - -const syncMsgBtn = document.getElementById('sync-msg') - -syncMsgBtn.addEventListener('click', () => { - const reply = ipcRenderer.sendSync('synchronous-message', 'ping') - const message = `Synchronous message reply: ${reply}` - document.getElementById('sync-reply').innerHTML = message -}) \ No newline at end of file diff --git a/docs/fiddles/ipc/pattern-1/index.html b/docs/fiddles/ipc/pattern-1/index.html new file mode 100644 index 0000000000000..28c1e42cd8b17 --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/index.html @@ -0,0 +1,14 @@ + + + + + + + Hello World! + + + Title: + + + + diff --git a/docs/fiddles/ipc/pattern-1/main.js b/docs/fiddles/ipc/pattern-1/main.js new file mode 100644 index 0000000000000..2e9e97edb81b0 --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/main.js @@ -0,0 +1,30 @@ +const {app, BrowserWindow, ipcMain} = require('electron') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + + ipcMain.on('set-title', (event, title) => { + const webContents = event.sender + const win = BrowserWindow.fromWebContents(webContents) + win.setTitle(title) + }) + + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-1/preload.js b/docs/fiddles/ipc/pattern-1/preload.js new file mode 100644 index 0000000000000..822f4ed51622b --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + setTitle: (title) => ipcRenderer.send('set-title', title) +}) diff --git a/docs/fiddles/ipc/pattern-1/renderer.js b/docs/fiddles/ipc/pattern-1/renderer.js new file mode 100644 index 0000000000000..c44f59a8df08c --- /dev/null +++ b/docs/fiddles/ipc/pattern-1/renderer.js @@ -0,0 +1,6 @@ +const setButton = document.getElementById('btn') +const titleInput = document.getElementById('title') +setButton.addEventListener('click', () => { + const title = titleInput.value + window.electronAPI.setTitle(title) +}); diff --git a/docs/fiddles/ipc/pattern-2/index.html b/docs/fiddles/ipc/pattern-2/index.html new file mode 100644 index 0000000000000..06e928c8ef13d --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/index.html @@ -0,0 +1,14 @@ + + + + + + + Dialog + + + + File path: + + + diff --git a/docs/fiddles/ipc/pattern-2/main.js b/docs/fiddles/ipc/pattern-2/main.js new file mode 100644 index 0000000000000..6f53c4adcce62 --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/main.js @@ -0,0 +1,32 @@ +const {app, BrowserWindow, ipcMain} = require('electron') +const path = require('path') + +async function handleFileOpen() { + const { canceled, filePaths } = await dialog.showOpenDialog() + if (canceled) { + return + } else { + 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() + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-2/preload.js b/docs/fiddles/ipc/pattern-2/preload.js new file mode 100644 index 0000000000000..cb78f84230f8b --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI',{ + openFile: () => ipcRenderer.invoke('dialog:openFile') +}) diff --git a/docs/fiddles/ipc/pattern-2/renderer.js b/docs/fiddles/ipc/pattern-2/renderer.js new file mode 100644 index 0000000000000..47712eefe7df1 --- /dev/null +++ b/docs/fiddles/ipc/pattern-2/renderer.js @@ -0,0 +1,7 @@ +const btn = document.getElementById('btn') +const filePathElement = document.getElementById('filePath') + +btn.addEventListener('click', async () => { + const filePath = await window.electronAPI.openFile() + filePathElement.innerText = filePath +}) diff --git a/docs/fiddles/ipc/pattern-3/index.html b/docs/fiddles/ipc/pattern-3/index.html new file mode 100644 index 0000000000000..18d2598986271 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/index.html @@ -0,0 +1,13 @@ + + + + + + + Menu Counter + + + Current value: 0 + + + diff --git a/docs/fiddles/ipc/pattern-3/main.js b/docs/fiddles/ipc/pattern-3/main.js new file mode 100644 index 0000000000000..13d7a1ab60cfb --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/main.js @@ -0,0 +1,48 @@ +const {app, BrowserWindow, Menu} = require('electron') +const path = require('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') + + // Open the DevTools. + mainWindow.webContents.openDevTools() +} + +app.whenReady().then(() => { + ipcMain.on('counter-value', (_event, value) => { + console.log(value) // will print value to Node console + }) + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/ipc/pattern-3/preload.js b/docs/fiddles/ipc/pattern-3/preload.js new file mode 100644 index 0000000000000..ad4dd27f1f9b2 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + handleCounter: (callback) => ipcRenderer.on('update-counter', callback) +}) diff --git a/docs/fiddles/ipc/pattern-3/renderer.js b/docs/fiddles/ipc/pattern-3/renderer.js new file mode 100644 index 0000000000000..3b184add08ba2 --- /dev/null +++ b/docs/fiddles/ipc/pattern-3/renderer.js @@ -0,0 +1,8 @@ +const counter = document.getElementById('counter') + +window.electronAPI.handleCounter((event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + event.reply('counter-value', newValue) +}) diff --git a/docs/tutorial/ipc.md b/docs/tutorial/ipc.md new file mode 100644 index 0000000000000..0189e0db7e806 --- /dev/null +++ b/docs/tutorial/ipc.md @@ -0,0 +1,571 @@ +--- +title: Inter-Process Communication +description: Use the ipcMain and ipcRenderer modules to communicate between Electron processes +slug: ipc +hide_title: false +--- + +# Inter-Process Communication + +Inter-process communication (IPC) is a key part of building feature-rich desktop applications +in Electron. Because the main and renderer processes have different responsibilities in +Electron's process model, IPC is the only way to perform many common tasks, such as calling a +native API from your UI or triggering changes in your web contents from native menus. + +## IPC channels + +In Electron, processes communicate by passing messages through developer-defined "channels" +with the [`ipcMain`] and [`ipcRenderer`] modules. These channels are +**arbitrary** (you can name them anything you want) and **bidirectional** (you can use the +same channel name for both modules). + +In this guide, we'll be going over some fundamental IPC patterns with concrete examples that +you can use as a reference for your app code. + +## Understanding context-isolated processes + +Before proceeding to implementation details, you should be familiar with the idea of using a +[preload script] to import Node.js and Electron modules in a context-isolated renderer process. + +* For a full overview of Electron's process model, you can read the [process model docs]. +* For a primer into exposing APIs from your preload script using the `contextBridge` module, check +out the [context isolation tutorial]. + +## Pattern 1: Renderer to main (one-way) + +To fire a one-way IPC message from a renderer process to the main process, you can use the +[`ipcRenderer.send`] API to send a message that is then received by the [`ipcMain.on`] API. + +You usually use this pattern to call a main process API from your web contents. We'll demonstrate +this pattern by creating a simple app that can programmatically change its window title. + +For this demo, you'll need to add code to your main process, your renderer process, and a preload +script. The full code is below, but we'll be explaining each file individually in the following +sections. + +```fiddle docs/fiddles/ipc/pattern-1 +``` + +### 1. Listen for events with `ipcMain.on` + +In the main process, set an IPC listener on the `set-title` channel with the `ipcMain.on` API: + +```javascript {6-10,22} title='main.js (Main Process)' +const {app, BrowserWindow, ipcMain} = require('electron') +const path = require('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() +} +//... +``` + +The above `handleSetTitle` callback has two parameters: an [IpcMainEvent] structure and a +`title` string. Whenever a message comes through the `set-title` channel, this function will +find the BrowserWindow instance attached to the message sender and use the `win.setTitle` +API on it. + +:::info +Make sure you're loading the `index.html` and `preload.js` entry points for the following steps! +::: + +### 2. Expose `ipcRenderer.send` via preload + +To send messages to the listener created above, you can use the `ipcRenderer.send` API. +By default, the renderer process has no Node.js or Electron module access. As an app developer, +you need to choose which APIs to expose from your preload script using the `contextBridge` API. + +In your preload script, add the following code, which will expose a global `window.electronAPI` +variable to your renderer process. + +```javascript title='preload.js (Preload Script)' +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + setTitle: (title) => ipcRenderer.send('set-title', title) +}) +``` + +At this point, you'll be able to use the `window.electronAPI.setTitle()` function in the renderer +process. + +:::caution Security warning +We don't directly expose the whole `ipcRenderer.send` API for [security reasons]. Make sure to +limit the renderer's access to Electron APIs as much as possible. +::: + +### 3. Build the renderer process UI + +In our BrowserWindow's loaded HTML file, add a basic user interface consisting of a text input +and a button: + +```html {11-12} title='index.html' + + + + + + + Hello World! + + + Title: + + + + +``` + +To make these elements interactive, we'll be adding a few lines of code in the imported +`renderer.js` file that leverages the `window.electronAPI` functionality exposed from the preload +script: + +```javascript title='renderer.js (Renderer Process)' +const setButton = document.getElementById('btn') +const titleInput = document.getElementById('title') +setButton.addEventListener('click', () => { + const title = titleInput.value + window.electronAPI.setTitle(title) +}); +``` + +At this point, your demo should be fully functional. Try using the input field and see what happens +to your BrowserWindow title! + +## Pattern 2: Renderer to main (two-way) + +A common application for two-way IPC is calling a main process module from your renderer process +code and waiting for a result. This can be done by using [`ipcRenderer.invoke`] paired with +[`ipcMain.handle`]. + +In the following example, we'll be opening a native file dialog from the renderer process and +returning the selected file's path. + +For this demo, you'll need to add code to your main process, your renderer process, and a preload +script. The full code is below, but we'll be explaining each file individually in the following +sections. + +```fiddle docs/fiddles/ipc/pattern-2 +``` + +### 1. Listen for events with `ipcMain.handle` + +In the main process, we'll be creating a `handleFileOpen()` function that calls +`dialog.showOpenDialog` and returns the value of the file path selected by the user. This function +is used as a callback whenever an `ipcRender.invoke` message is sent through the `dialog:openFile` +channel from the renderer process. The return value is then returned as a Promise to the original +`invoke` call. + +:::caution A word on error handling +Errors thrown through `handle` in the main process are not transparent as they +are serialized and only the `message` property from the original error is +provided to the renderer process. Please refer to +[#24427](https://github.com/electron/electron/issues/24427) for details. +::: + +```javascript {6-13,25} title='main.js (Main Process)' +const { BrowserWindow, dialog, ipcMain } = require('electron') +const path = require('path') + +//... + +async function handleFileOpen() { + const { canceled, filePaths } = await dialog.showOpenDialog() + if (canceled) { + return + } else { + return filePaths[0] + } +} + +function createWindow () { + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(__dirname, 'preload.js') + } + }) + mainWindow.loadFile('index.html') +} + +app.whenReady(() => { + ipcMain.handle('dialog:openFile', handleFileOpen) + createWindow() +}) +//... +``` + +:::tip on channel names +The `dialog:` prefix on the IPC channel name has no effect on the code. It only serves +as a namespace that helps with code readability. +::: + +:::info +Make sure you're loading the `index.html` and `preload.js` entry points for the following steps! +::: + +### 2. Expose `ipcRenderer.invoke` via preload + +In the preload script, we expose a one-line `openFile` function that calls and returns the value of +`ipcRenderer.invoke('dialog:openFile')`. We'll be using this API in the next step to call the +native dialog from our renderer's user interface. + +```javascript title='preload.js (Preload Script)' +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + openFile: () => ipcRenderer.invoke('dialog:openFile') +}) +``` + +:::caution Security warning +We don't directly expose the whole `ipcRenderer.invoke` API for [security reasons]. Make sure to +limit the renderer's access to Electron APIs as much as possible. +::: + +### 3. Build the renderer process UI + +Finally, let's build the HTML file that we load into our BrowserWindow. + +```html {10-11} title='index.html' + + + + + + + Dialog + + + + File path: + + + +``` + +The UI consists of a single `#btn` button element that will be used to trigger our preload API, and +a `#filePath` element that will be used to display the path of the selected file. Making these +pieces work will take a few lines of code in the renderer process script: + +```javascript title='renderer.js (Renderer Process)' +const btn = document.getElementById('btn') +const filePathElement = document.getElementById('filePath') + +btn.addEventListener('click', async () => { + const filePath = await window.electronAPI.openFile() + filePathElement.innerText = filePath +}) +``` + +In the above snippet, we listen for clicks on the `#btn` button, and call our +`window.electronAPI.openFile()` API to activate the native Open File dialog. We then display the +selected file path in the `#filePath` element. + +### Note: legacy approaches + +The `ipcRenderer.invoke` API was added in Electron 7 as a developer-friendly way to tackle two-way +IPC from the renderer process. However, there exist a couple alternative approaches to this IPC +pattern. + +:::warning Avoid legacy approaches if possible +We recommend using `ipcRenderer.invoke` whenever possible. The following two-way renderer-to-main +patterns are documented for historical purposes. +::: + +:::info +For the following examples, we're calling `ipcRenderer` directly from the preload script to keep +the code samples small. +::: + +#### Using `ipcRenderer.send` + +The `ipcRenderer.send` API that we used for single-way communication can also be leveraged to +perform two-way communication. This was the recommended way for asynchronous two-way communication +via IPC prior to Electron 7. + +```javascript title='preload.js (Preload Script)' +// You can also put expose this code to the renderer +// process with the `contextBridge` API +const { ipcRenderer } = require('electron') + +ipcRenderer.on('asynchronous-reply', (_event, arg) => { + console.log(arg) // prints "pong" in the DevTools console +}) +ipcRenderer.send('asynchronous-message', 'ping') +``` + +```javascript title='main.js (Main Process)' +ipcMain.on('asynchronous-message', (event, arg) => { + console.log(arg) // prints "ping" in the Node console + // works like `send`, but returning a message back + // to the renderer that sent the original message + event.reply('asynchronous-reply', 'pong') +}) +``` + +There are a couple downsides to this approach: + +* You need to set up a second `ipcRenderer.on` listener to handle the response in the renderer +process. With `invoke`, you get the response value returned as a Promise to the original API call. +* There's no obvious way to pair the `asynchronous-reply` message to the original +`asynchronous-message` one. If you have very frequent messages going back and forth through these +channels, you would need to add additional app code to track each call and response individually. + +#### Using `ipcRenderer.sendSync` + +The `ipcRenderer.sendSync` API sends a message to the main process and waits _synchronously_ for a +response. + +```javascript title='main.js (Main Process)' +const { ipcMain } = require('electron') +ipcMain.on('synchronous-message', (event, arg) => { + console.log(arg) // prints "ping" in the Node console + event.returnValue = 'pong' +}) +``` + +```javascript title='preload.js (Preload Script)' +// You can also put expose this code to the renderer +// process with the `contextBridge` API +const { ipcRenderer } = require('electron') + +const result = ipcRenderer.sendSync('synchronous-message', 'ping') +console.log(result) // prints "pong" in the DevTools console +``` + +The structure of this code is very similar to the `invoke` model, but we recommend +**avoiding this API** for performance reasons. Its synchronous nature means that it'll block the +renderer process until a reply is received. + +## Pattern 3: Main to renderer + +When sending a message from the main process to a renderer process, you need to specify which +renderer is receiving the message. Messages need to be sent to a renderer process +via its [`WebContents`] instance. This WebContents instance contains a [`send`][webcontents-send] method +that can be used in the same way as `ipcRenderer.send`. + +To demonstrate this pattern, we'll be building a number counter controlled by the native operating +system menu. + +For this demo, you'll need to add code to your main process, your renderer process, and a preload +script. The full code is below, but we'll be explaining each file individually in the following +sections. + +```fiddle docs/fiddles/ipc/pattern-3 +``` + +### 1. Send messages with the `webContents` module + +For this demo, we'll need to first build a custom menu in the main process using Electron's `Menu` +module that uses the `webContents.send` API to send an IPC message from the main process to the +target renderer. + +```javascript {11-26} title='main.js (Main Process)' +const {app, BrowserWindow, Menu} = require('electron') +const path = require('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') +} +//... + +``` + +For the purposes of the tutorial, it's important to note that the `click` handler +sends a message (either `1` or `-1`) to the renderer process through the `counter` channel. + +```javascript +click: () => mainWindow.webContents.send('update-counter', -1) +``` + +:::info +Make sure you're loading the `index.html` and `preload.js` entry points for the following steps! +::: + +### 2. Expose `ipcRenderer.on` via preload + +Like in the previous renderer-to-main example, we use the `contextBridge` and `ipcRenderer` +modules in the preload script to expose IPC functionality to the renderer process: + +```javascript title='preload.js (Preload Script)' +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback) +}) +``` + +After loading the preload script, your renderer process should have access to the +`window.electronAPI.onUpdateCounter()` listener function. + +:::caution Security warning +We don't directly expose the whole `ipcRenderer.on` API for [security reasons]. Make sure to +limit the renderer's access to Electron APIs as much as possible. +::: + +:::info +In the case of this minimal example, you can call `ipcRenderer.on` directly in the preload script +rather than exposing it over the context bridge. + +```javascript title='preload.js (Preload Script)' +const { ipcRenderer } = require('electron') + +window.addEventListener('DOMContentLoaded', () => { + const counter = document.getElementById('counter') + ipcRenderer.on('update-counter', (_event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + }) +}) +``` + +However, this approach has limited flexibility compared to exposing your preload APIs +over the context bridge, since your listener can't directly interact with your renderer code. +::: + +### 3. Build the renderer process UI + +To tie it all together, we'll create an interface in the loaded HTML file that contains a +`#counter` element that we'll use to display the values: + +```html {10} title='index.html' + + + + + + + Menu Counter + + + Current value: 0 + + + +``` + +Finally, to make the values update in the HTML document, we'll add a few lines of DOM manipulation +so that the value of the `#counter` element is updated whenever we fire an `update-counter` event. + +```javascript title='renderer.js (Renderer Process)' +const counter = document.getElementById('counter') + +window.electronAPI.onUpdateCounter((_event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue +}) +``` + +In the above code, we're passing in a callback to the `window.electronAPI.onUpdateCounter` function +exposed from our preload script. The second `value` parameter corresponds to the `1` or `-1` we +were passing in from the `webContents.send` call from the native menu. + +### Optional: returning a reply + +There's no equivalent for `ipcRenderer.invoke` for main-to-renderer IPC. Instead, you can +send a reply back to the main process from within the `ipcRenderer.on` callback. + +We can demonstrate this with slight modifications to the code from the previous example. In the +renderer process, use the `event` parameter to send a reply back to the main process through the +`counter-value` channel. + +```javascript title='renderer.js (Renderer Process)' +const counter = document.getElementById('counter') + +window.electronAPI.onUpdateCounter((event, value) => { + const oldValue = Number(counter.innerText) + const newValue = oldValue + value + counter.innerText = newValue + event.reply('counter-value', newValue) +}) +``` + +In the main process, listen for `counter-value` events and handle them appropriately. + +```javascript title='main.js (Main Process)' +//... +ipcMain.on('counter-value', (_event, value) => { + console.log(value) // will print value to Node console +}) +//... +``` + +## Pattern 4: Renderer to renderer + +There's no direct way to send messages between renderer processes in Electron using the `ipcMain` +and `ipcRenderer` modules. To achieve this, you have two options: + +* Use the main process as a message broker between renderers. This would involve sending a message +from one renderer to the main process, which would forward the message to the other renderer. +* Pass a [MessagePort] from the main process to both renderers. This will allow direct communication +between renderers after the initial setup. + +## Object serialization + +Electron's IPC implementation uses the HTML standard +[Structured Clone Algorithm][sca] to serialize objects passed between processes, meaning that +only certain types of objects can be passed through IPC channels. + +In particular, DOM objects (e.g. `Element`, `Location` and `DOMMatrix`), Node.js objects +backed by C++ classes (e.g. `process.env`, some members of `Stream`), and Electron objects +backed by C++ classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not serializable +with Structured Clone. + +[context isolation tutorial]: context-isolation.md +[security reasons]: ./context-isolation.md#security-considerations +[`ipcMain`]: ../api/ipc-main.md +[`ipcMain.handle`]: ../api/ipc-main.md#ipcmainhandlechannel-listener +[`ipcMain.on`]: ../api/ipc-main.md +[IpcMainEvent]: ../api/structures/ipc-main-event.md +[`ipcRenderer`]: ../api/ipc-renderer.md +[`ipcRenderer.invoke`]: ../api/ipc-renderer.md#ipcrendererinvokechannel-args +[`ipcRenderer.send`]: ../api/ipc-renderer.md +[MessagePort]: ./message-ports.md +[preload script]: process-model.md#preload-scripts +[process model docs]: process-model.md +[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[`WebContents`]: ../api/web-contents.md +[webcontents-send]: ../api/web-contents.md#contentssendchannel-args