Skip to main content

Electron: Executing Main Process Code from Renderer

Why Execute Main Process Code from Renderer?

With Electron v20 and later, the render processes have been isolated from the main process (the node process), meaning that the render process no longer has access to node and electron methods directly. These changes enhance security, but also increase the difficults of working with packages like electron-store which require access to these methods.

tip

This method is not necessary for all packages, only those that require access to the node methods in the main process.

In a nutshell, the main process is the node process that runs the Electron app, and the render process is the browser window that displays the app. The main process has access to the node and electron methods, while the render process does not.

Many common libraries will require access to these methods, such as electron-store, which is a popular package for storing data in an Electron app. Now we cannot simply import electron-store into the render process and use it as we did before. We have to run it in the main process and expose the methods that we want to use in the render process.

To accomplish this we need to use the ipcRenderer and ipcMain Electron modules to communicate between the two processes, and explicitly list every method that we want to expose to the renderer process.

The documentation covering this process is a bit sparse, so I wanted to write a guide to help others who are struggling with this.

Let's start with some simple test examples, and then we will show how to use this method with the electron-store package.

Simple Example

Let's start with some simple console log examples.

Preload Script

We can start in the preload script by using contextBridge.exposeInMainWorld to create an API as the documentation suggests.

📘 electronjs.org > context-isolation

📘 electron-vite.org > using preload scripts

preload.ts
import {ipcRenderer, contextBridge} from 'electron';

// 💣 WARNING: pretty much anything besides imports here will crash the preload script and prevent it from loading
// everything in this script generator must be contained in contextBridge function.

contextBridge.exposeInMainWorld('test', {
// example: invoking function directly in preload.ts
sing: () => console.log("i'm singing 🎵"),
// example: request function invoke in main.ts
run: () => ipcRenderer.invoke('run'),
});

Main Process

We place the ipcMain.handle method in the main process to handle the run method that we exposed in the preload script. Note that the handler is placed in the createWindow function, which is called when the window is created.

main.ts
function createWindow() {
win = new BrowserWindow({
width: 1200,
height: 1200,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
contextIsolation: true,
},
});

ipcMain.handle('run', () => {
return '👟 running...';
});
}

Renderer Process

Note that the sing test, which was returned directly from the preload script, can be called synchronously.

Now that we have exposed the run method in the main process, we can call it from the renderer process.

There are a couple different async syntaxes that will work for this.

some-component-in-renderer.ts
// example: invoking function in preload.ts
window.test.sing();

// example: invoking function in main.ts
// invoke method 1
const run = async () => {
const response = await window.test.run();
console.log('invoke method 1:', response);
};
run();

// example: invoking function in main.ts
// invoke method 2
window.test.run().then((response) => {
console.log('invoke method 2:', response);
});

Using with electron-store

Ok that's all clear, but how do we use this with packages like electron-store where we have to import the package, invoke a client and which has a lot of methods that we want to expose?

Here is a quick copy paste of the electron-store example from the documentation just so you can see what we are trying to accomplish.

import Store from 'electron-store';

const store = new Store();

store.set('unicorn', '🦄');
console.log(store.get('unicorn'));
//=> '🦄'

// Use dot-notation to access nested properties
store.set('foo.bar', true);
console.log(store.get('foo'));
//=> {bar: true}

store.delete('unicorn');
console.log(store.get('unicorn'));
//=> undefined

Additionally we need to be able to pass values for these methods to the main process and receive the results back in the renderer process.

Preload Script

preload.ts
contextBridge.exposeInMainWorld('store', {
set: (key: string, value: any) => ipcRenderer.invoke('store-set', key, value),
get: (key: string) => ipcRenderer.invoke('store-get', key),
delete: (key: string) => ipcRenderer.invoke('store-delete', key),
});

Main Process

main.ts
function createWindow() {
win = new BrowserWindow({
width: 1200,
height: 1200,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
contextIsolation: true,
},
});

ipcMain.handle('store-set', (event, key: string, value: any) => {
store.set(key, value);
});

ipcMain.handle('store-get', (event, key: string) => {
return store.get(key);
});

ipcMain.handle('store-delete', (event, key: string) => {
store.delete(key);
});

//...
}

Renderer Process

some-component-in-renderer.ts
// Set a value in the store
window.store.set('unicorn', '🦄').then(() => {
console.log('Value set successfully');
});

// Get a value from the store
window.store.get('unicorn').then((value) => {
console.log('Unicorn:', value);
});

// Delete a value from the store
window.store.delete('unicorn').then(() => {
console.log('Value deleted');
});

Return Function and Not Result

Sometimes we may want to return the function itself and not the result of the function like we did in the store.get example. This can help make our code in the renderer more composable.

Let's say for example we wanted to use the app.getPath("userData") method, I may want to call this function directly in my renderer process so I can do things like construct more complicated filepath to submit to another function that generates and saves files.

To do this we can modify our code in the preload.ts file like so._createMdxContent

preload.ts
contextBridge.exposeInMainWorld('app', {
getPath: (pathVariable: string) => {
return ipcRenderer.invoke('getPath', pathVariable);
},
});

Note the key difference here is that we are returning the ipcRenderer.invoke call instead of the result of the ipcRenderer.invoke call.

Then in our main.ts file we can handle the getPath method like so.

main.ts
ipcMain.handle('getPath', (event, pathVariable: string) => {
return app.getPath(pathVariable);
});

And finally in our some-component-in-renderer.ts file we can call the getPath method like so.

some-component-in-renderer.ts
window.app.getPath('userData').then((path) => {
console.log('User Data Path:', path);
});

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More