Two Approaches for Opening Multiple Electron Windows in a React/Electron Desktop Application


Recently I’ve had to figure out how to open multiple Electron windows for use in a React/Electron application. This turned out to be simpler and easier than I’d originally thought but finding the simple solution did take some searching around, since I didn’t see any good examples. In fact, many of the examples I came across took me in a direction that ultimately was more complicated and frustrating than necessary.

There are two ways that I know of to open a new browser window if you’re using React/Electron:

  1. Enabling nativeWindowOpen in your main BrowserWindow, using JS window.open() to open a window and tying the window body to your app using createPortal from the ‘react-dom’ package.
  2. Using an Electron Interprocess Communication (IPC) to send a message to your electron event handler and spawning a new BrowserWindow.

My hope here is to provide the example for others to use, so they don’t have to figure it out from scratch as I did. Either way may work for your situation but there are some things you should know about each.

Method 1: Native Browser Window

What most of the Examples and Message Board Posts Say to Do

The examples I saw suggested updating the main.js where your Electron application is initialized, to allow native browser windows to open, using window.open() to open a standard JavaScript window, then using React createPortal to link some react component(s) to the body of the window.

BrowserWindow Settings

let window = new BrowserWindow({ 
                width: 700, height: 500, 
                show: false, 
                title: 'my title', 
                webPreferences: { 
                  nodeIntegration: true, 
                  webSecurity: false, 
                  nativeWindowOpen: true 
                }
              });

Standalone Window Component

Ideally, to do the native window method, you’ll want to create a component to encapsulate the window handling. Perhaps something like this would get you started.

import React, { useState, useEffect} from 'react';
import {createPortal } from 'react-router';
/**
 * COMPONENT:  STANDALONE WINDOW
 * PURPOSE:   DISPLAY THE CHILD CONTENT IN A SEPARATE BROWSER WINDOW
 */
function StandaloneWindow({ children }) {
  /**@type {[Window, React.Dispatch<Window>]} */
  const [windowHandle, setWindowHandle] = useState(null);
  
  // SIDE EFFECT: WHEN COMPONENT MOUNTS OPEN A WINDOW
  //              WHEN THE COMPONENT UNMOUNTS, CLOSE THE WINDOW
  useEffect(() => {
    let w = window.open('');
    copyStyles(window.document, w.document);
    setWindowHandle(w);
    return () => {
      if(windowHandle) {
        windowHandle.close();
      }
    }
  }, []);
  /**
   * FUNCTION:  COPY STYLES
   * PURPOSE:   COPY THE STYLE CONFIGURATION FROM ONE DOCUMENT TO 
   *            ANOTHER SO THE TARGET DOCUMENT WILL MATCH 
   *            THE SOURCE DOCUMENT
   * @param {Document} source
   * @param {Document} target
   */
  function copyStyles(source, target) {
    Array.from(source.querySelectorAll('link[rel="stylesheet"], style'))
    .forEach(link => {
      target.head.appendChild(link.cloneNode(true));
    });
  }
  if(windowHandle) {
    return createPortal(children, windowHandle.document.body);
  }
}

export default StandaloneWindow;

Rendering Your Component Using the StandaloneWindow Component and React Context

It is advantageous to create a React context and context provider, to hold references to any windows you create. This way one could simply add the windows to a collection in a high level context and have them render. It is possible to add to that collection from anywhere beneath the context provider.

If routes are used in the application then the routes can be placed inside of the context provider you create so the windows can persist even if the user changes routes; this way the windows won’t go out of scope and therefore go blank.

A barebones example of a context and context provider would be something like the following.

import React, { useState, useEffect } from 'react';

import StandaloneWindow from './StandaloneWindow';

// BASIC CONTEXT TO STORE THE POPUP WINDOWS AND PROVIDER A
// FUNCTION TO CREATE A NEW POPUP WINDOW AND HANDLE THE 
// BUSINESS LOGIC OF SAVING IT IN THE COLLECTION
export const AppContext = React.createContext({
  popupWindows: [],
  createPopupWindow: () => {}
});

// APP CONTEXT PROVIDER WHICH CAN BE USED TO ACCESS THE CONTEXT
export const AppContextProvider = ({ children }) => {

  function createPopupWindow(contents) {
    let newWindow = <StandaloneWindow>{contents}</StandaloneWindow>;
  setState((oldState) => (...oldState, pupupWindows: [...oldState.popupWindow, newWindow])
  }
  return (
    <AppContext.Provider value={state}>
      {children}
    </AppContext.Provider>
  ); 
}
export { AppContext, AppContextProvider };

Rendering The Popup Windows Stored in Your Context Provider

Once the infrastructure to store the windows is in place you need to have the windows inside of a rendering function, so the app will be able to render and update the windows as needed.

Here’s an example of how this could be done in our app’s index.js, where all of our routes are defined.

import React, { useContext } from 'react';
import { render } from 'react-dom';
import { HashRouter as Router, Route Switch } from 'react-router-dom';
import { AppContextProvider, AppContext } from './AppContext';


function App() {
  return(
    <AppContextProvider>
      <Router>
        <Switch>
          <Route path="/LandingPage" component={LandingPage} />
        </Switch>
      </Router>
      <PopupWindows />  
    </AppContextProvider>  
  );
}
/**
 * COMPONENT: POPUP WINDOWS
 * PURPOSE:   DISPLAY ANY POPUP WINDOWS IN THE APP CONTEXT
 */
function PopupWindows() {
  const { popupWindows } = useContext(AppContext);
  return (<>{popupWindows}</>); 
}

render(<App />, document.getElementById('root'));

Generating a Popup From a Within a Component

The following barebones example will generate a popup by calling the createPopupWindow() function from the AppContext. This will then popup a new window and display “Hello World”.

import React, { useContext } from 'react';
import { Button } from 'react-bootstrap';

function MyTestComponent() {
  let { createPopupWindow } = useContext(AppContext); 
  function getPopup() {
    createPopupWindow(<h1>Hello World</h1>);
  }
  return (<Button onClick={() => getPopup()}>Click Me</Button>);
}
export default MyTestComponent;

Conclusions About the Native Browser Window / createPortal Method

The native browser window method is complicated and may cause unexpected errors. In my own work, I was attempting to show C3 charts inside of one of these windows and I could not get the C3 charts to display. I ultimately had to abandon this method because it wasn’t worth trying to figure out what was going on with it.

Method 2: Using Electron’s Inter-Process Communications (IPC) To Create a New Electron BrowserWindow

After abandoning method 1, I found the IPC method of spawning a window and displaying components to be far superior both in terms of simplicity as well as reliability. Anything that works in your main react/electron app should work in one of these windows, provided you initialize everything as needed.

Updates to the Electron Main.js to Facilitate Multiple Windows

Your main.js may be quite different from this but this example below should give an idea of how you could use the IPC functionality of Electron to create new windows whenever it receives a new message from your application.

const { app, BrowserWindow, ipcMain, Menu } = require('electron'); 
const uuid = require('uuid/v4');

/**@type {{id: String, window: BrowserWindow}[]}
let windows = [];
let dev = false;
/**
 * FUNCTION: CREATE REACT APP WINDOW
 * PURPOSE:  REUSABLE FUNCTION FOR CREATING NEW ELECTRON BrowserWindow INSTANCES
 */
function createReactAppWindow(windows, { dev, baseDir, reactRouterPath, reactRouteParams, windowTitle, windowWidth, windowHeight }) {
  let id = uuid();
  let window = new BrowserWindow({
    width: windowWidth,
    height: windowHeight,
    show: false,
    title: windowTitle,
    webPreferences: {
      nodeIntegration: true,
      webSecurity: false,
      nativeWindowOpen: false,
    }
  });
  browserWindowCollection.push({id, window});

  let indexPath;
  // DEV MODE, IF WE ARE RUNNING FROM SOURCE
  if(dev && process.argv.indexOf('--noDevServer') === -1) {
    indexPath = url.format({
      protocol: 'http:',
      host: 'localhost:8080',
      pathname: 'index.html',
      slashes: 'true'
    });
  // IF WE ARE IN PRODUCTION MODE, RUNNING A BUILT VERSION OF THE APP
  } else {
    indexPath = url.format({
      protocol: 'file:',
      pathname: path.join(baseDir, 'dist', 'index.html'),
      slashes: true
    });
  }

  // THIS IS WHERE WE CUSTOMIZE WHICH WINDOW WILL BE DISPLAYED BY 
  // USING REACT HASH ROUTER PATH / PARAMETERS
  // THIS WILL BE LOADED ON THE REACT SIDE TO RENDER THE RIGHT
  // COMPONENTS FOR THE USER
  if( reactRouterPath) {
    indexPath = indexPath.concat(`#${reactRouterPath}${reactRouteParams ? `/${reactRouteParams}` : ''}` );
  }
  window.loadURL(indexPath);
}

// A BASIC FUNCTION TO CREATE THE MAIN WINDOW FOR OUR APP WHICH
// THE USER SEES WHEN THEY OPEN THE APPLICATION
function createMainWindow() {
  createReactAppWindow(windows, { dev, baseDir: __dirname, reactRouterPath: '/SplashPage', reactRouteParams: null, windowTitle: 'my main window' })
}

// WHEN THE APP FIRST ACTIVATES OPEN THE WINDOW
app.on('activate', () => {
  if(windows.length === 0) {
    createMainWindow();
  }
});

// HERE WE HAVE AN IPC CHANNEL CALLED 'NEW_WINDOW' WHICH WE WILL 
// SEND MESSAGES TO FROM OUR APP, IN ORDER TO CREATE THE WINDOW
// FOR EACH KIND OF WINDOW WE WANT, WE NEED TO HAVE ANOTHER case HERE
// THIS WILL CALL OUR FUNCTION TO CREATE THE WINDOW WHENEVER IT 
// RECEIVES A MESSAGE
ipcMain.on('NEW_WINDOW', (event, arg) => {
  var argObject = arg[0];
  if(argObject && argObject.type) {
    switch(argObject.type) {
      case 'MY_NEW_WINDOW_1_TYPE':
        createReactAppWindow(windows, { dev, baseDir: __dirname, reactRouterPath: '/MyPath1', reactRouteParams: argObject.param, windowTitle: 'my test window' });
        break;
    }
  }
});

Updates to Your App to Trigger the New Window Functionality

Now that we have electron ready to make new windows, we need to update our application to be able to trigger new windows to be created. It’s probably best to encapsulate this functionality in a custom react hook, to separate concerns within your application.

Custom React Hook to Trigger Window Creation

Here’s an example of the custom hook you could create to trigger a window.

import { ipcRenderer } from 'electron';

function useWindowManagement() {
  
  function OpenMyTestWindow(myID) {
    let paramObj = { type: 'MY_NEW_WINDOW_1_TYPE', param: myID };
    ipcRenderer.send('NEW_WINDOW', [paramObj]); 
  }

  return { OpenMyTestWindow };
}
Using The Custom Window Creation Hook in a Component
import React from 'react';
 import { Button } from 'react-bootstrap';
 // THE CUSTOM HOOKS FROM THE EXAMPLE ABOVE
 import {useWindowManagement } from './WindowManagement.hooks';
 // A BAREBONES TEST COMPONENT TO CALL THE CUSTOM HOOK
 function MyComponent() {
   return(<Button onClick={() => OpenMyTestWindow('1')} />);
 }
Creating React Routes to Display Our Components In the Windows

Now that the groundwork has been laid to display the window, we need to have some paths for react to utilize for choosing which components to display when the window opens.

In this example, the default path electron will send the user to will be SplashPage while the popup will send the user to MyComponentToDisplay.

import React from 'react';
 import { render } from 'react-dom';
 import { HashRouter as Router, Route Switch } from 'react-router-dom';
 import {MyComponentToDisplay} from './MyComponentToDisplay';
 import {SplashPage} from './SplashPage';

function App() {
  return(
  <Router>
    <Switch>
      <Route path="/SplashPage" component={SplashPage} />
      <Route path="/MyPath1" component={MyComponentToDisplay} />
    </Switch>
  </Router>
  );
}

Conclusions about the IPC Method of Creating New Windows in React/Electron

This method appears to have the greatest simplicity, though if you have complicated lists of parameters there could be some challenges in this version of the solution.

It seems as though, in most situations, one would want to create a new window using an ID as a parameter and this would work quite well.


Leave a Reply

Your email address will not be published. Required fields are marked *