Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

IPC: from the page to the main process

IPC is messaging between processes. The most common case: the page needs the main process to do something and return a result, for example read the notes. For this the main process registers a handler with ipcMain.handle, and the page calls it with ipcRenderer.invoke and gets a promise with the answer.

The handler in the main process

Messages are told apart by a channel name — an ordinary string. The usual style is topic:action. Add to main.js:

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
 
let notes = ['Buy bread', 'Call Anna'];
 
app.whenReady().then(() => {
  ipcMain.handle('notes:load', () => {
    return notes;
  });
 
  createWindow();
});

The handler runs for every request on the notes:load channel. Whatever it returns goes to the page. For now the notes are kept in the main process's memory.

The bridge in preload

The page cannot see ipcRenderer, so preload gives it a wrapper function:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');
 
contextBridge.exposeInMainWorld('notes', {
  load: () => ipcRenderer.invoke('notes:load'),
});

invoke sends the request and returns a promise that resolves with the handler's answer.

Calling from the page

// renderer.js
const list = document.querySelector('#notes');
 
async function showNotes() {
  const notes = await window.notes.load();
  list.replaceChildren();
  for (const note of notes) {
    const li = document.createElement('li');
    li.textContent = note;
    list.append(li);
  }
}
 
showNotes();

After startup the window shows the two notes from the main process.

Passing data

The arguments of invoke reach the handler after the event object:

// main.js
ipcMain.handle('notes:add', (event, text) => {
  notes.push(text);
  return notes;
});
// preload.js
contextBridge.exposeInMainWorld('notes', {
  load: () => ipcRenderer.invoke('notes:load'),
  add: (text) => ipcRenderer.invoke('notes:add', text),
});

On the page the form calls await window.notes.add(text.value) and then showNotes(). Data is passed as a copy: strings, numbers, arrays and plain objects go through, functions and DOM elements do not.

Errors

If the handler throws, the promise on the page is rejected:

// main.js
ipcMain.handle('notes:add', (event, text) => {
  if (text.trim() === '') {
    throw new Error('Empty note');
  }
  notes.push(text);
  return notes;
});

On the page such a call is wrapped in try...catch and the message is shown to the user.

Common mistake

If you call ipcMain.handle inside createWindow, then when the window is created again (for example on the activate event on macOS) Electron throws Attempted to register a second handler for 'notes:load'. A channel can have only one handler — register it once after whenReady, outside the window-creating function.

Thanks for reading!

Donate to the site — we work hard for you.