How to make electron multiple window application

How to make electron multiple window application

How to create a multi-window electron app and synchronize state using IPC

ยท

5 min read

Introduction

In this blog, you will learn to build a cross-platform app where users can interact with multiple windows using electron. We will use an electron react boilerplate. We will dive into Webpack configs, and context bridge API. We will write our code in React and it will compile and run in electron app

Use Cases

App with multiple windows is already being used in Manufacturing, Healthcare, and other Industry for various analyses.

Brief Intro To Electron

Main Process

Each Electron app has a single main process, which acts as the application's entry point. The main process runs in a Node.js environment.

Renderer Process

A renderer is responsible for rendering web content.

Process Electron.jpg

Let's dive directly into code now ๐Ÿ˜„

1) Create electron react boilerplate. Run the below command in PowerShell or terminal in the sequence

  • git clone --depth 1 --branch main https://github.com/electron-react-boilerplate/electron-react-boilerplate.git your-project-name
  • cd your-project-name
  • npm install

2) Start your server. You will see a window like this.

  • npm start

image.png

3) We will make some modifications to App.tsx. We will add a new button, when the user clicks on the new button it will open a new electron window.

Inside App.tsx.


// Hello Component

const args = 1;
  const createNewWindow = () => {
    window.electron.ipcRenderer.sendMessage('ipc-create-new-window', [args]);
  };

return (
    <div>
      <div className="Hello">
        <img width="200" alt="icon" src={icon} />
      </div>
      <h1>electron-react-boilerplate</h1>
      <div className="Hello">
        <button type="button" onClick={createNewWindow}>
          <span role="img" aria-label="books">
            ๐Ÿ“š
          </span>
          Open Second Window
        </button>
      </div>
    </div>
  );

// ... rest of code

If you are wondering what is window.electron.ipcRenderer then have a look at preload.js. Preload scripts contain code that executes in a renderer process before its web content begins loading. We can use the electron IPC message communication mechanism using contextBridge.exposeInMainWorld. It attaches them to the global Window object

We are not done yet. We need to tell our main process (main.ts) to create a new Window.

Inside main.ts


// Listen for ipc-create-new-window event
ipcMain.on('ipc-create-new-window', async (event, arg) => {

  function createNewWindow() {
    const RESOURCES_PATH = app.isPackaged
      ? path.join(process.resourcesPath, 'assets')
      : path.join(__dirname, '../../assets');

    const getAssetPath = (...paths: string[]): string => {
      return path.join(RESOURCES_PATH, ...paths);
    };

    let newWindow: BrowserWindow | null = new BrowserWindow({
      show: false,
      width: 1024,
      height: 728,
      icon: getAssetPath('icon.png'),
      webPreferences: {
        preload: app.isPackaged
          ? path.join(__dirname, 'preload.js')
          : path.join(__dirname, '../../.erb/dll/preload.js'),
      },
    });

    newWindow.loadURL(resolveHtmlPath('index.html'));

    newWindow.on('ready-to-show', () => {
      if (!newWindow) {
        throw new Error('"newWindow" is not defined');
      }
      if (process.env.START_MINIMIZED) {
        newWindow.minimize();
      } else {
        newWindow.show();
      }
    });

    newWindow.on('closed', () => {
      newWindow = null;
    });
  }
  try {
    createNewWindow();
  } catch (e) {
    console.log({ e });
  }
});

Now clicking on the button will open a new electron Window. Wait a sec ๐Ÿ˜– both of the windows have the same content. Is it possible to load a different web component (component or page)? The answer is Yes ๐Ÿ˜„

First, why do both windows have the same content? Because we told the new window to pickup same the index.html

image.png

How electron uses index.html ?

Our index.tsx is compiled into a javascript file called renderer.js and then it is used by index.html. This index.html is used by our electron window. Electron has loadURL(htmlfile) method for this specific purpose.

Fig 6 How webpack turn react code into electron usable

You probably guessed by now, to show different components or web content in the new window we need to tell Electron to use different HTML files and renderer.js. But it does not end here, we also have to tweak the webpack config. Why? Beacuse turning react code into js file so that it can be used by electron is achieved by webpack.

Webpack

Webpack config files are inside .erb folder. We will tweak webpack.config.renderer.dev.ts. An entry point indicates which module webpack should use to begin building out its internal dependency graph. The output property tells webpack where to emit the bundles it creates and how to name these files. Html webpack plugin is used to inject renderer.js as a javascript executable inside html


const configuration: webpack.Configuration = {

 // rest of code ...
  entry: {
    renderer: [
      `webpack-dev-server/client?http://localhost:${port}/dist`,
      'webpack/hot/only-dev-server',
      path.join(webpackPaths.srcRendererPath, 'index.tsx'),
    ],
    renderer2: [
      `webpack-dev-server/client?http://localhost:${port}/dist`,
      'webpack/hot/only-dev-server',
      path.join(webpackPaths.srcRendererPath, 'index2.tsx'),
    ],
  },

 output: {
    path: webpackPaths.distRendererPath,
    publicPath: '/',
    filename: '[name].dev.js',
    library: {
      type: 'umd',
    },
  },

// rest of code ...

plugins: [

// rest of code ..

 new HtmlWebpackPlugin({
      filename: path.join('index.html'),
      template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
      },
      isBrowser: false,
      env: process.env.NODE_ENV,
      chunks: ['renderer'],
      isDevelopment: process.env.NODE_ENV !== 'production',
      nodeModules: webpackPaths.appNodeModulesPath,
    }),

    new HtmlWebpackPlugin({
      filename: path.join('index2.html'),
      template: path.join(webpackPaths.srcRendererPath, 'index2.ejs'),
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
      },
      isBrowser: false,
      chunks: ['renderer2'],
      env: process.env.NODE_ENV,
      isDevelopment: process.env.NODE_ENV !== 'production',
      nodeModules: webpackPaths.appNodeModulesPath,
    }),

// rest of code..
}

Creating HTML file for second window

CreateIndex2.tsx, index2.ejs and SecondWindow.tsx inside src/renderer

image.png

index2.ejs

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-inline'"
    />
    <title>Second Window</title>
  </head>
  <body>
    <div id="root2"></div>
  </body>
</html>

Index2.tsx

import { createRoot } from 'react-dom/client';
import SecondWindow from './SecondWindow';

const container = document.getElementById('root2')!;
console.log({ container });

const root = createRoot(container);
root.render(<SecondWindow />);

SecondWindow.tsx

import React from 'react';

const SecondWindow = () => {
  return <div>SecondWindow</div>;
};

export default SecondWindow;

Now click on Open Second Window button. It will open a new window and render SecondWindow.tsx

image.png

Code is here github

Upcoming Part 2 : How to sync state across multiple window

ย