Tutorial: Setting Up a Multi-Threaded IPC Call in an Electron App
This tutorial will guide you through setting up a multi-threaded inter-process communication (IPC) call in an Electron app. We’ll create a button in a React component that triggers an asynchronous operation. The operation will communicate with the main process using IPC and execute a task in a separate thread with a worker. Progress updates and the final result will be sent back and displayed in the UI.
1. Setting Up the React Component
We’ll create a React component with a button. On click, the button will invoke an asynchronous method exposed via the window API.
Code: React Component
import React, { useState } from "react";
const AsyncButtonWithIpc = () => {
const [progress, setProgress] = useState<number | null>(null);
const [result, setResult] = useState<string | null>(null);
const handleClick = async () => {
try {
setProgress(0);
setResult(null);
const count = 10; // Example: Requesting 10 random numbers
const progressController = {
onProgress: (update: number) => {
setProgress((prev) => (prev ?? 0) + update);
},
};
const response = await window.getRandomNosInParallel(count, progressController);
setResult(response);
setProgress(null);
} catch (error) {
console.error("Error during async operation:", error);
}
};
return (
<div>
<button onClick={handleClick}>Generate Random Numbers</button>
{progress !== null && <p>Progress: {progress}%</p>}
{result && <p>Result: {result}</p>}
</div>
);
};
export default AsyncButtonWithIpc;
2. Exposing the API in the Preload Script
Electron allows you to securely expose APIs to the renderer process using the preload script. We’ll define a getRandomNosInParallel function that invokes an IPC call.
Code: Preload Script
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("getRandomNosInParallel", (count, progressController) => {
return ipcRenderer.invoke("getRandomNosInParallel", count, progressController);
});
3. Handling the IPC Call in the Main Process
The main process will listen for the getRandomNosInParallel event using ipcMain.handle. It will create a worker thread to perform the task and manage progress updates.
Code: Main Process Handler
import { ipcMain } from "electron";
import { Worker } from "worker_threads";
ipcMain.handle("getRandomNosInParallel", async (event, count: number, progressEventsConfig: { onProgress: (update: number) => void }) => {
return new Promise((resolve, reject) => {
const worker = new Worker("./workers/randomNumberGenerator.js");
const cleanup = () => {
worker.removeAllListeners();
worker.terminate();
};
worker.on("message", (message) => {
if (message.type === "progress") {
progressEventsConfig.onProgress(message.update);
} else if (message.type === "result") {
cleanup();
resolve(message.data);
}
});
worker.on("error", (error) => {
cleanup();
reject(error);
});
worker.postMessage({ count });
});
});
4. Performing the Task in a Worker Thread
The worker thread will handle the computationally heavy task. It will send periodic progress updates and the final result back to the main process.
Code: Worker Thread Implementation
const { parentPort } = require("worker_threads");
parentPort.on("message", ({ count }) => {
const randomNumbers = [];
const progressStep = Math.ceil(count / 10); // Send progress every 10% of the task
for (let i = 0; i < count; i++) {
// Simulating computation
randomNumbers.push(Math.random());
if ((i + 1) % progressStep === 0) {
parentPort.postMessage({ type: "progress", update: 10 }); // Update by 10%
}
}
parentPort.postMessage({ type: "result", data: randomNumbers });
});
5. Putting It All Together
Diagram: UI process ↔ Main process ↔ Worker thread pool (include message paths and transfer lists)

Once all the components are in place:
- The React component triggers the
getRandomNosInParallelAPI. - The API invokes an IPC call to the main process.
- The main process creates a worker thread to perform the task.
- The worker thread sends progress updates and the final result back to the main process.
- The main process resolves the promise and returns the result to the renderer process, which updates the UI.
6. Example Folder Structure
Here’s how the files should be organized:
src/
├── components/
│ └── AsyncButtonWithIpc.tsx
├── main-process/
│ └── async-ipc-handler.ts
├── workers/
│ └── randomNumberGenerator.js
└── preload.js
7. Running the App
-
Ensure the preload script is specified in your
BrowserWindowconfiguration:const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, enableRemoteModule: false, }, }); -
Start the Electron app and click the button in the UI. You should see progress updates and the final result displayed.
8. Enhancements
- Error Handling: Add more robust error handling for worker crashes and IPC failures.
- Performance: Use transferable objects (e.g.,
ArrayBuffer) for zero-copy transfers if dealing with large data. - Flexibility: Implement cancellation or timeout mechanisms for long-running tasks.
Summary
With this setup, you’ve implemented a multi-threaded IPC call in an Electron app. This architecture keeps the UI responsive by offloading heavy tasks to worker threads, while progress updates and the final result are seamlessly communicated back to the renderer process.