reactjsasynchronouselectronipcrendereripcmain

Electron with React - ipcRenderer receives a reply from a previous message to ipcMain?


So I'm pretty new to Electron, and my goal is to have a completely offline application that effectively queries and displays results from a loaded SQLite file (hence the questionable SQL practices here).

I am able to query my database and get expected returns. However, when I make consecutive calls like this, the second query is getting the same result as the previous call. Here is the bit from my renderer (React). The first query will usually as expected, but then the second res is identical to that of the first. However, sometimes the second query gets the expected result and the first is identical to that. Either way, they both end up with the same res value.

Any ideas on what exactly is happening and how I can fix this? I don't believe a synchronous approach would be possible with ipcMain.

Code in renderer (React)

// called from ipcMain when database file is set
ipcRenderer.on('set-db', (event, arg) => {
    // Get authors
    queryDB("SELECT * FROM users;")
        .then((res) => {
            try {
                var options = [];
                res.forEach((e) => {
                    options.push({value: e.id, label: `@${e.name}`});
                    if (e.display_name != e.name) {
                        options.push({value: e.id, label: `@${e.display_name}`});
                    }
                });
                setAuthorOptions(options);
                console.log("Set author options");
            }
            catch (exception) {}
        });

    // Get channels
    queryDB("SELECT * FROM channels;")
        .then((res) => {
            try {
                var options = [];
                res.forEach((e) => {
                    if (allowedChannelTypes.includes(e.type)) {
                        options.push({value: e.id, label: `# ${e.name}`});
                    }
                });
                setChannelOptions(options);
                console.log("Set channel options");
            }
            catch (exception) {}
        });

});

Here is the code in the main process

ipcMain.on('asynchronous-message', (event, sql) => {
    if (db) {
        db.all(sql, (error, rows) => {
            event.reply('asynchronous-reply', (error && error.message) || rows);
        });
    }
    return
});

And the renderer code

export default function queryDB(sql) {
    return new Promise((res) => {
        ipcRenderer.once('asynchronous-reply', (_, arg) => {
            res(arg);
        });

        ipcRenderer.send('asynchronous-message', sql);
    })
}

Solution

  • The problem is that you are using ipcRenderer.once several times with the same channel name. As described in the docs, this method:

    Adds a one time listener function for the event. This listener is invoked only the next time a message is sent to channel, after which it is removed.

    So if you make a new call before the previous one got a reply, they will both receive the results of the first one which is answered.

    I'm not familiar with SQLite, but from what I gathered, depending on the library you use db.all() will either work with a callback or be a promise. I see two ways to fix this:

    With a promise

    If you use the promise way, you can simply use invoke/handle. For example:

    Renderer

    export default function queryDB(sql) {
        return ipcRenderer.invoke('query-db', sql);
    }
    

    Main

    ipcMain.handle('query-db', (event, sql) => {
        if(!db) return;
        return db.all(sql);
    });
    

    With a callback

    If you prefer to use a callback, you have to make sure that you use unique channel names with ipcRenderer.once, for example:

    Renderer

    export default function queryDB(sql) {
        return new Promise((res) => {
            const channelName = 'asynchronous-reply-' + Date.now();
    
            ipcRenderer.once(channelName, (_, arg) => {
                res(arg);
            });
    
            ipcRenderer.send('asynchronous-message', sql, channelName);
        })
    }
    

    Main

    ipcMain.on('asynchronous-message', (event, sql, channelName) => {
        if (db) {
            db.all(sql, (error, rows) => {
                event.reply(channelName, (error && error.message) || rows);
            });
        }
        return
    });
    

    If you use this method, and since you don't always send a reply to the created channel, you also need to make sure you cleanup the unused listeners using ipcRenderer.removeListener or ipcRenderer.removeAllListeners.