Skip to content

Sending message from Main to Renderer

Could someone help me out here. I’m completely confused on how to solve this. I’ve now spent about a week trying to find a solution for this but have come up short and there appears to be a lack of a solid solution online. I’ve made a github repository trying to demonstrate the issue.

In short I’ve implemented a status bar in my application which i want to populate with various string messages. These messages would be sent from functions that are contained inside a js file that imports electron, which mean’s it doesn’t have direct access to the Renderer. So how would I send these messages to the Renderer. I’m assuming this needs to be done using the ContextBridge, but i have no clue how to successfully do this, so if your response is just linking me to the context bridge docs, don’t bother, lol I’ve exhausted myself looking at that. The other alternative i was considering is using a custom event but but i’m not sure that would solve the problem either.

Here is a sample of what im trying to do along with repo on github. If you do a pull-request to fix the repo, ill gladly merge and keep the repo public for others to benefit from and share with the community.

As a minor problem, im not sure why i can no longer call getPath from ‘app’ from within a js file that’s not loaded into the render thread.

I trigger a method from Renderer


const doWork = () => {


import { contextBridge } from "electron";

const messenger = require("../src/helpers/messenger");
contextBridge.exposeInMainWorld("messenger", messenger);


const { app } = require("electron");
const path = require("path");

// using electron module to demonstrate this file can't be imported into renderer
export function showMessage(msg) {
  const dir = path.join(app.getPath("documents"), "presets");
  // TODO: send message to renderer...

export function doWork() {
  console.log("Doing working...");

  // step 1: long process
  showMessage("Processing step 1...");

  // step 2: long process
  showMessage("Processing step 2...");

  // step 3: long process
  showMessage("Processing step 3...");

I’d like to display the messages sent from the main to renderer to be displayed in the status bar of


        <span class="text-caption">Show message here...</span>

** UPDATE 01 **

For some reason my message is not being received in the Renderer. Here are my code changes


import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  setStatus: (callback, func) =>
    ipcRenderer.on("set-status", (event, ...args) => func(...args)),


  <q-page class="flex flex-center">
    <q-btn label="Show Message" @click="doWork" />

import { defineComponent } from "vue";

export default defineComponent({

  setup() {
    // send message for testing...
    const doWork = () => {

    // recieve status messages...
    window.electronAPI.setStatus("set-status", (data) => {
      console.log("STATUS:", data);
      // TODO $store.dispatch("....");

    return {


A technique that works for me is not to use the preload.js script to define concrete implementations. Instead, I use the preload.js script to only define channel (names) that I can communicate with between the main and render threads. IE: Seperating your concerns. Implement your concrete functions within your main thread scripts and render thread scripts.


// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send': [],
        // From main to render.
        'receive': [
            'message:update' // Here is your channel name
        // From render to main and back again.
        'sendReceive': []

// Exposed protected methods in the render process.
    // Allowed 'ipcRenderer' methods.
    'ipcRender', {
        // From render to main.
        send: (channel, args) => {
            let validChannels = ipc.render.send;
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, args);
        // From main to render.
        receive: (channel, listener) => {
            let validChannels = ipc.render.receive;
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender`.
                ipcRenderer.on(channel, (event, ...args) => listener(...args));
        // From render to main and back again.
        invoke: (channel, args) => {
            let validChannels = ipc.render.sendReceive;
            if (validChannels.includes(channel)) {
                return ipcRenderer.invoke(channel, args);

Note: Though I do not use Vue.js, you should get the gist of the below two files.

main.js (main thread)

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;

const nodePath = require("path");

let window;

function createWindow() {
    const window = new electronBrowserWindow({
        x: 0,
        y: 0,
        width: 800,
        height: 600,
        show: false,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true, 
            preload: nodePath.join(__dirname, 'preload.js')

        .then(() => {; });

    return window;

electronApp.on('ready', () => {
    window = createWindow();
    // Send a message to the window.
    window.webContents.send('message:update', 'Doing work...');

electronApp.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {

electronApp.on('activate', () => {
    if (electronBrowserWindow.getAllWindows().length === 0) {

index.html (render thread)

<!DOCTYPE html>
<html lang="en">
        <meta charset="UTF-8">
        <span id="text-caption">Show message here...</span>
        // Listen for message updates from the main thread.
        window.ipcRender.receive('message:update', (message) => {
            document.getElementById('text-caption').innerText = message;