How to Send Messages Between Electron Windows

Jason Sturges
JavaScript in Plain English
4 min readMar 4, 2021

--

Photo by Maria Teneva on Unsplash

Copy clipboard data between between React apps in multiple windows.

Multiple windows in Electron don’t share the same data model, and must use inter process communication to share data.

Even though all windows come from the same application, each window is an instance of BrowserWindow containing a unique Chromium web view. Sending messages between windows is similar to tabs or windows in a web browser.

In a web browser, you can communicate using BroadcastChannels.

After defining a channel, posted messages dispatch events for listeners:

const channel = new BroadcastChannel("mychannel");// Send a message:
channel.postMessage("Hello, world");
// Receive a message:
channel.onmessage = (event) => {
console.log(event);
});

In Electron, a similar strategy is leveraged by sending messages from the rendering process to the main process. Then, the main process sends messages to all render windows.

Clipboard Example

For example, consider advanced clipboard data that we want to share between windows. Each window may be running unique apps; or, multiple instances of the same app.

I’m going to continue from my Multiple Window Electron App post:

In that example, new windows can be opened via File ≫ New Window, which effectively creates a new instance of the exact same React app.

Sending Data

From any window’s web contents, serialize a data payload to JSON and send it through the ipcRenderer to a desired channel. In this example, the client is going to send the clipboard data using clipboard-send .

const { ipcRenderer } = require('electron');const copy = (data) => {
const json = JSON.stringify(data);
ipcRenderer.send("clipboard-send", json);
}

In the main thread (main.dev.js), create a listener on ipcMain to handle clipboard-send channel events that were dispatched from any window’s renderer process. Then, relay that data back to all windows by sending clipboard-receive channel events:

const { ipcMain } = require('electron');

ipcMain.on('clipboard-send', (event, json) => {
windows.forEach(window => {
window.send('clipboard-receive', json);
});
});

Receiving Data

Now with the main thread sending the message to all windows, each window needs to listen for events on the clipboard-receive channel:

const { ipcRenderer } = require('electron');let clipboard = {};ipcRenderer.on('clipboard-receive', (event, json) => {
if (!json) return;
const data = JSON.parse(json);
clipboard = data;
});

Each window’s renderer thread can now deserialize the data payload, and cache it locally in the clipboard.

Full lifecycle:

  • One window’s renderer thread sends a message to the main thread
  • Main thread echos the message to all windows
  • All windows receive the event with data payload

New Windows

One gotcha’ here are windows created after data has been broadcast. Continuing with the clipboard example, let’s say:

  • One window copied data to the clipboard
  • A new window was opened
  • User tries to paste in the new window — that window hadn’t received any event since it was created after the message was broadcast.

In this case, you can create a local cache in the main renderer; then, automatically send the clipboard data when new windows are created.

const { ipcMain } = require('electron');let clipboardCache;export const createWindow = async () => {
let newWindow = new BrowserWindow();
// ...
newWindow.webContents.on('did-finish-load', () => {
// Send clipboard data
newWindow.send('clipboard-receive', clipboardCache);
});
}

Putting it All Together

Within JavaScript code displayed in your windows, such as a React app, send and receive events to the main thread. This resembles communication between client and server:

const { ipcRenderer } = require('electron');// Local clipboard cache
let clipboard = {};
// Send clipboard data to the main thread
const copy = (data) => {
const json = JSON.stringify(data);
ipcRenderer.send("clipboard-send", json);
};
// Listen for incoming clipboard data
ipcRenderer.on('clipboard-receive', (event, json) => {
if (!json) return;

const data = JSON.parse(json);
clipboard = data;
});
// Do something with the clipboard data
const paste = () => {
if (!clipboard) return;
// ...
};

In the main thread (main.dev.js), adding to the previous multiple example:

const { ipcMain } = require('electron');// Clipboard cache
let clipboard;
const windows = new Set();export const createWindow = async () => {
if (
process.env.NODE_ENV === 'development' ||
process.env.DEBUG_PROD === 'true'
) {
await installExtensions();
}
let x, y; const currentWindow = BrowserWindow.getFocusedWindow(); if (currentWindow) {
const [currentWindowX, currentWindowY] = currentWindow.getPosition();
x = currentWindowX + 24;
y = currentWindowY + 24;
}
let newWindow = new BrowserWindow({
show: false,
width: 1200,
height: 812,
x,
y,
webPreferences: {
nodeIntegration: true
}
});
newWindow.loadURL(`file://${__dirname}/app.html`); newWindow.webContents.on('did-finish-load', () => {
if (!newWindow) {
throw new Error('"newWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
newWindow.minimize();
} else {
newWindow.show();
newWindow.focus();
}
// Send clipboard data to new window
newWindow.send('clipboard-receive', clipboard);
}); newWindow.on('closed', () => {
windows.delete(newWindow);
newWindow = null;
});
newWindow.on('focus', () => {
const menuBuilder = new MenuBuilder(newWindow);
menuBuilder.buildMenu();
});
windows.add(newWindow);
return newWindow;
};
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (windows.size === 0) createWindow();
});

// Echo clipboard data back to all windows:
ipcMain.on('clipboard-send', (event, json) => {
clipboard = json;
windows.forEach(window => {
window.send('clipboard-receive', json);
});
});

--

--

Avant-garde experimental artist — creative professional leveraging technology for immersive experiences