Prevent duplicate instances of the same window, invoked by different processes, in an electron app.
- Author: Ganesh Rathinavel
- License: MIT
- Repo URL: https://github.com/ganeshrvel/tutorial-electron-window-switching
- Contacts: ganeshrvel@outlook.com
The challenge
Electron has two types of processes —“ Main” and “Renderer”.
Since these processes are isolated from each other, a child window instance has to be loaded separately for both processes. This raises another issue — duplication of the windows with the same URL origin
I was able to solve it with a bit of workaround. This was originally implemented inside OpenMTP — Advanced Android File Transfer Application for macOS.
Implementation:
Here, as an example, I will be creating a privacy policy window which can be invoked by both menu item and as well as an anchor tag.
- Install packages
$ npm install react-helmetor$ yarn add react-helmet
- Create a file create-windows.js and add the below code
import { BrowserWindow, remote } from 'electron';
const PRIVACY_POLICY_PAGE_TITLE = `Privacy Policy`; // this will be used as an identifier to capture the same browser instance
let privacyPolicyWindow = null;/**
* Privacy Policy Window
*/const undefinedOrNull = _var => {
return typeof _var === "undefined" || _var === null;
};export const loadExistingWindow = (allWindows, title) => {
if (!undefinedOrNull(allWindows)) {
for (let i = 0; i < allWindows.length; i += 1) {
const item = allWindows[i];
if (item.getTitle().indexOf(title) !== -1) {
item.focus();
item.show();
return item;
}
}
}
return null;
}; return item;
}
}
} return null;
};return loadExistingWindow(allWindows, PRIVACY_POLICY_PAGE_TITLE)
? null
: new remote.BrowserWindow(config);
}// incoming call from the main process
const allWindows = BrowserWindow.getAllWindows();return loadExistingWindow(allWindows, PRIVACY_POLICY_PAGE_TITLE)
? null
: new BrowserWindow(config);
};const privacyPolicyCreateWindow = (isRenderedPage = false) => {
const config = {
width: 800,
height: 600,
minWidth: 600,
minHeight: 400,
show: false,
resizable: true,
title: `${APP_TITLE}`,
minimizable: true,
fullscreenable: true,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
},
backgroundColor: getWindowBackgroundColor(),
};
// incoming call from a rendered page
if (isRenderedPage) {
const allWindows = remote.BrowserWindow.getAllWindows();
const existingWindow = loadExistingWindow(
allWindows,
PRIVACY_POLICY_PAGE_TITLE
);
return {
windowObj: existingWindow ?? new remote.BrowserWindow(config),
isExisting: !!existingWindow,
};
}
// incoming call from the main process
const allWindows = BrowserWindow.getAllWindows();
const existingWindow = loadExistingWindow(
allWindows,
PRIVACY_POLICY_PAGE_TITLE
);
return {
windowObj: existingWindow ?? new BrowserWindow(config),
isExisting: !!existingWindow,
};
};
export const privacyPolicyWindow = (isRenderedPage = false, focus = true) => {
try {
if (_privacyPolicyWindow) {
if (focus) {
_privacyPolicyWindow.focus();
_privacyPolicyWindow.show();
}
return _privacyPolicyWindow;
}
// show the existing _privacyPolicyWindow
const { windowObj, isExisting } = privacyPolicyCreateWindow(isRenderedPage);
// return the existing windowObj object
if (isExisting) {
return windowObj;
}
_privacyPolicyWindow = windowObj; _privacyPolicyWindow.loadURL(
`file://path/to/my-app/app/index.html#privacyPolicyPage`); // @todo: change the path accordinglyexport const privacyPolicyCreateWindow = (isRenderedPage = false) => {
try {
if (privacyPolicyWindow) {
privacyPolicyWindow.focus();
privacyPolicyWindow.show();
return privacyPolicyWindow;
}// show the existing privacyPolicyWindow
const _privacyPolicyWindowTemp = createWindow(isRenderedPage);
if (!_privacyPolicyWindowTemp) {
return privacyPolicyWindow;
}privacyPolicyWindow = _privacyPolicyWindowTemp;
privacyPolicyWindow.loadURL(
`file://path/to/my-app/app/index.html#privacyPolicyPage`
); // @todo: change the path accordingly
privacyPolicyWindow.webContents.on("did-finish-load", () => {
privacyPolicyWindow.show();
privacyPolicyWindow.focus();
});privacyPolicyWindow.onerror = error => {
console.error(error, `createWindows -> privacyPolicyWindow -> onerror`);
};privacyPolicyWindow.on("closed", () => {
privacyPolicyWindow = null;
});return privacyPolicyWindow;
} catch (e) {
console.error(e, `createWindows -> privacyPolicyWindow`);
}
};// Add to your menu
submenu: [
{
label: 'Privacy Policy from main process',
click: () => {
privacyPolicyWindow(false);
}
}
]
- The PrivacyPolicyPage/index.jsx
import React, { Component } from 'react';
import { Helmet } from 'react-helmet';const PRIVACY_POLICY_PAGE_TITLE = `Privacy Policy`;class PrivacyPolicyPage extends Component {
render() {
return (
<div>
<Helmet titleTemplate={`%s`}>
<title>{PRIVACY_POLICY_PAGE_TITLE}</title>
</Helmet>
<div>
<p>Window body</p>
</div>
</div>
);
}
}export default PrivacyPolicyPage;
- Edit your router file
import PrivacyPolicyPage from './PrivacyPolicyPage';//Add the route<Switch>
<Route
key={PrivacyPolicyPage}
path='/privacyPolicyPage'
exact
component={PrivacyPolicyPage}
/>
</Switch>
- Add an anchor tag inside your home page
import { privacyPolicyWindow } from './create-windows';<a
className={styles.a}
onClick={() => {
privacyPolicyWindow(true);
}}
>
Privacy Policy from the renderer process
</a>
Conclusion
- call privacyPolicyCreateWindow(true) from renderer process and call privacyPolicyCreateWindow(false) from the main process.
- privacyPolicyCreateWindow method will look up for the existing windows which match the title string and it returns the pre-existing window if it finds any.
More repos
- OpenMTP — Advanced Android File Transfer Application for macOS
- Tutorial Series by Ganesh Rathinavel
- npm: electron-root-path
- Electron React Redux Advanced Boilerplate
License
tutorial-electron-window-switching is released under MIT License.
Copyright © 2018-Present Ganesh Rathinavel