How to make electron multiple window application
How to create a multi-window electron app and synchronize state using IPC
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.
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
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
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.
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
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
Code is here github
Upcoming Part 2 : How to sync state across multiple window