Search code examples
electron

Electron IPC messages are remembered and reinvoked multiple times


I have an Electron app that has multiple buttons that generate documents. The documents are really just HTML pages that are loaded in a new BrowserWindow and receive custom parameters via IPC, which are then injected into the document by javascript.

For passing parameters via IPC, I'm using a channel that I call 'document-params'. All documents use this channel. My expectation is that a message sent to a channel will be consumed by its listeners and disappear from whatever IPC queue Electron is using. Instead, it's as if all messages sent to that channel remain in the channel. Each new invocation of a document receives all previous messages in the channel, along with the latest one. If I invoke the same document multiple times, its data gets inserted into the document multiple times. If I invoke different documents, I can see the injection function of the previous document failing to run under the current document (calls to document.getElementById()) fail.

As far as I can tell, I'm following the Electron IPC tutorial. I see no mention anywhere in the documentation that messages are retained in the IPC channels, which is counter intuitive behavior anyway.

Here's how my code is setup:

I have 2 documents, each defined as its own entry point in package.json:

{
    "name": "document_contract",
    "html": "./src/renderer/windows/documents/Contract.html",
    "js": "./src/renderer/windows/documents/renderer.js",
    "preload": {
        "js": "./src/renderer/windows/documents/preload.js"
    }
},
{
    "name": "document_review_draft",
    "html": "./src/renderer/windows/documents/ReviewDraft.html",
    "js": "./src/renderer/windows/documents/renderer.js",
    "preload": {
        "js": "./src/renderer/windows/documents/preload.js"
    }
}

Here are the renderer.js and preload.js files

/src/renderer/windows/documents/preload.js:

const { contextBridge } = require('electron');
import { preloadCommon } from '../preload.js';


contextBridge.exposeInMainWorld('electronAPI', {...preloadCommon});

../preload.js:

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

export const preloadCommon = {
    subscribe: (channel, callback) => {
        ipcRenderer.on(channel, callback);
    },
    getPopupParams: () => {
        ipcRenderer.send('popup-params');
    },
    getDocumentParams: () => {
        ipcRenderer.send('document-params');
    },
    popup: (name, data) => {
        ipcRenderer.send('popup', name, data);
    },
    generateDocument: (template, title, data) => {
        ipcRenderer.send('document', template, title, data);
    }
};

/src/renderer/windows/documents/renderer.js:

import '../renderer.js';

function bootstrapContract(data) {
    // Document appropriate DOM manipulation, such as injecting <li> elements from data
}

function bootstrapReviewDraft(data) {
    // Document appropriate DOM manipulation, such as injecting <li> elements from data
}

function dispatch(template, data) {
    switch (template) {
        case 'contract':
            bootstrapContract(data);
            break;
        case 'review-draft':
            bootstrapReviewDraft(data);
            break;
        default:
            break;
    }
}

window.electronAPI.subscribe('document-params', (event, template, data) => dispatch(template, data));
window.electronAPI.getDocumentParams();

On the main process, here's how the channels are handled:

const { ipcMain, BrowserWindow } = require("electron");
const path = require('path');

let popup;

const templates = {
    'contract': DOCUMENT_CONTRACT_WEBPACK_ENTRY,
    'review-draft': DOCUMENT_REVIEW_DRAFT_WEBPACK_ENTRY,
};

const createDocument = (template, title, data) => {
    popup = new BrowserWindow({
        show: true,
        webPreferences: {
            preload: DOCUMENT_CONTRACT_PRELOAD_WEBPACK_ENTRY
        },
        title: title,
        parent: global.mainWindow,
        modal: true,
    });
    popup.loadURL(templates[template]);
    popup.maximize();
    popup.show();
};

ipcMain.on('document', (event, template, title, data) => {
    const onPassParamsToDocument = (event) => {
        event.sender.send('document-params', template, data);
    };
    ipcMain.on('document-params', onPassParamsToDocument);
    createDocument(template, title, data);
});

Solution

  • On your main, everytime you receive a message on the "document" channel, you create a new listener to "document-params". Since you never remove these listeners, you can be sure it leads to the problems you have.

    IPC listeners can be removed using ipcMain.removeListener() or ipcMain.removeAllListeners().

    Your implementation also seems over complicated, you should probably consider using invoke/handle rather than send/on/on when you call main and expect a response.