Clemens Dinkel
Clemens Dinkel

Reputation: 344

Sending messages from main process to renderer in Electron in 2022

In my App I have two windows: mainWindow and actionWindow. On my mainWindow I use the ipcRenderer.on listener to receive as message from the main process when the actionWindow is closed. The message however doesn't come through.

The mainWindow is used to control actions that take place on the actionWindow (e.g. navigate to an URL, remotely close the window, ...). I want to give the user the power to move and close the actionWindow manually as well, which is why its title bar is visible and usable.

I expose ipcRenderer.invoke for two-way communication and ipcRenderer.on to the mainWindow's renderer via contextBridge in a preload file.

This is what the code looks like (based on vite-electron-builder template)

main process

const mainWindow = new BrowserWindow({
  show: false, // Use 'ready-to-show' event to show window
  webPreferences: {
    nativeWindowOpen: true,
    webviewTag: false,
    preload: join(__dirname, "../../preload/dist/index.cjs"),
  },
});
const actionWindow = new BrowserWindow({
  // some props
})
actionWindow.on("close", () => {
  console.log("window closed")
  mainWindow.webContents.send("closed", { message: "window closed" });
});

preload

contextBridge.exposeInMainWorld("ipcRenderer", {
  invoke: ipcRenderer.invoke,
  on: ipcRenderer.on,
});

renderer (mainWindow)

window.ipcRenderer.on("closed", () => {
  console.log("message received")
  // do something
  });

I know for a fact that

To me this means that either

So either my code is buggy or Electron has recently put some restrictions on one of these methods which I'm not aware of.

Any ideas? If there is a smarter way to do this than IPC I'm also open to that.

Upvotes: 6

Views: 1483

Answers (1)

Clemens Dinkel
Clemens Dinkel

Reputation: 344

Ok after hours of searching, trying and suffering I (almost accidentaly) found a solution to my problem. It really seems to be the case that electron simply doesn't do anything anymore when you call the on method from your renderer. Studying the docs about contextBridge again I saw that the way I exposed invoke and on to the renderer, was considered bad code. The safer way to do this is expose a function for EVERY ipc channel you want to use. In my case using TypeScript it looks like this:

preload

contextBridge.exposeInMainWorld("ipcRenderer", {
  invokeOpen: async (optionsString: string) => {
    await ipcRenderer.invoke("open", optionsString);
  },
  onClose: (callback: () => void) => {
    ipcRenderer.on("closed", callback);
  },
  removeOnClose: (callback: () => void) => {
    ipcRenderer.removeListener("closed", callback);
  },
});

renderer(mainWindow)

window.ipcRenderer.onClose(() => {
  // do sth
});
window.ipcRenderer.invokeOpen(JSON.stringify(someData)).then(() => {
  // do sth when response came back
});

NOTE: To prevent memory leaks by creating listeners on every render of the mainWindow you also have to use a cleanup function which is provided with removeOnClose (see preload). How to use this function differs depending on the frontend framework. Using React it looks like this:

const doSth= () => {
    console.log("doing something")
    ...
  };

  useEffect(() => {
    window.ipcRenderer.onClose(doSth);
    return () => {
      window.ipcRenderer.removeOnClose(doSth);
    };
  }, []);

Not only is this a safer solution, it actually suddenly works :O Using the cleanup function we also take care of leaks.

Upvotes: 4

Related Questions