Search code examples
javascriptnode.jssecurityelectronipc

Electron contextBridge returns undefined


I've got these 4 project files:

main.js
preload.js
renderer.js
index.html

Node: 17.4.0 Electron: 18.2.0

I'm attempting to open a text file on my filesystem, triggered by a click event from renderer.js - then load the text file's contents into a <div> tag in index.html.

main.js

const {app, BrowserWindow, ipcMain} = require("electron");
const path = require("path");
const fs = require("fs");

const createWindow = () => {
    // Create the browser window.
    const mainWindow = new BrowserWindow({
         webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            contextIsolation: true,
            nodeIntegration: false
         }
    });
    mainWindow.loadFile(path.join(__dirname, "index.html"));

    // Open the DevTools.
    mainWindow.webContents.openDevTools();
};

app.on("ready", () => {
    createWindow();

    app.on("activate", () => {
        if (BrowserWindow.getAllWindows().length === 0) createWindow();
    });
});


function openFile(){
    fs.readFile("logs.txt", "utf8", (err, data) => {
        if (err) {
            console.error(err);
            return "Error Loading Log File";
        }
        console.log(data);
        return data;
    });
}

ipcMain.handle("channel-load-file", openFile);

preload.js

const {contextBridge, ipcRenderer} = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
    loadFile: () => ipcRenderer.invoke("channel-load-file")
});

renderer.js

const btn = document.querySelector("#btn");
btn.addEventListener("click", e => {
   let data = window.electronAPI.loadFile();
   document.getElementById("main-content").innerText = data;
});

I can definitely see the contents of the Log file inside console.log(data); in the main.js

But, the <div id="main-content"></div> gets populated with undefined.

I believe I'm missing some crucial step within either: preload.js or renderer.js

Anyone see where the chain of events is getting lost?

(I'm very open to any improvements to my flow)


Solution

  • Inserting console.log()'s in the code below indicates that the handle content is executed before openFile has a chance to return a result.

    main.js (main process)

    function openFile() {
        fs.readFile("logs.txt", "utf-8", (err, data) => {
            if (err) {
                console.error(err);
                return "Error Loading Log File";
            }
    
            console.log('openFile: ' + data); // Testing
    
            return data;
        });
    }
    
    ipcMain.handle('channel-load-file', () => {
        let result = openFile();
    
        console.log('handle: ' + result); // Testing
    
        return result;
    })
    

    The console.log() results are...

    handle: undefined
    openFile: File content...
    

    To fix this, let's change fs.readFile from a callback to a promise, so we can await for it in the handle.

    As the handle is dealing with a promise, let's use the syntactic sugar async and await for easier implementation.

    main.js (main process)

    function openFile() {
        return new Promise((resolve, reject) => {
            fs.readFile("logs.txt", "utf-8", (error, data) => {
                if (error) {
                    console.log('reject: ' + error); // Testing
                    reject(error);
                } else {
                    console.log('resolve: ' + data); // Testing
                    resolve(data)
                }
            });
        });
    }
    
    ipcMain.handle('channel-load-file', async (event, message) => {
        return await openFile()
            .then((data) => {
                console.log('handle: ' + data); // Testing
                return data;
            })
            .catch((error) => {
                console.log('handle error: ' + error); // Testing
                return 'Error Loading Log File';
            })
    });
    

    Lastly, let's modify the way we retrieve the data in the index.html file.

    PS: Let's also add .toString() to the returned data (just to be sure).

    index.html (render process)

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Electron Test</title>
            <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
        </head>
    
        <body>
            <div id="main-content"></div>
    
            <input type="button" id="button" value="Load File">
        </body>
    
        <script>
            document.getElementById('button').addEventListener('click', () => {
                window.electronAPI.loadFile()
                    .then((data) => {
                        console.log(data); // Testing
                        document.getElementById("main-content").innerText = data.toString();
                    });
            })
        </script>
    </html>