electron: Error while importing electron in react | import { ipcRenderer } from 'electron'

I have created a simple react app with create-react-app and I have integrated it with electron successfully. Everything was working great until I tried to import electron inside the action creator file. If I remove the line below, the app works fine. The problem is that I can’t use the ipcRenderer to communicate from the react side to the electron main process.

This line causes the app to crash: import { ipcRenderer } from 'electron';

I get the following error:

TypeError: fs.existsSync is not a function (anonymous function) node_modules/electron/index.js:6

  3 | 
  4 | var pathFile = path.join(__dirname, 'path.txt')
  5 | 
> 6 | if (fs.existsSync(pathFile)) {
  7 |   module.exports = path.join(__dirname, fs.readFileSync(pathFile, 'utf-8'))
  8 | } else {
  9 |   throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')

I found out on Google that this is a common problem when trying to import electron.

Thanks for the help

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 51
  • Comments: 88 (4 by maintainers)

Commits related to this issue

Most upvoted comments

2022 edit

I’ve posted a new history / step-by-step understanding of the Electron framework and good secure coding practices with the framework as it stands today as an additional resource here. This resource is a good resource for beginners or those who are not familiar with the Electron framework.

Original reply

I hope that this comment get noticed, because a lot of people are asking about importing fs or ipcRenderer in your apps. It’s a common-need for electron apps but I found not many people have got it right, and are using outdated patterns. tl;dr - there is a security vulnerability if you don’t import your node module (ie. fs) or electron module (ie. ipcRenderer) in the correct way. If you are using your app for only yourself you are probably safe, but if you ever want to share or sell your app you should read ahead.

Our goal

Before I go into the solution, it’s important to understand why we are doing this in the first place. Electron apps allow us to include node modules in our apps, which gives them amazing power, but security concerns. We want to allow our app to use native-os (ie. node) features, but we don’t want them to be abused.

As brought up by @raddevus in a comment, this is necessary when loading remote content. If your electron app is entirely offline/local, then you are probably okay simply turning on nodeIntegration:true. I still would, however, opt to keep nodeIntegration:false to act as a safeguard for accidental/malicious users using your app, and prevent any possible malware that might ever get installed on your machine from interacting with your electron app and using the nodeIntegration:true attack vector (incredibly rare, but could happen)!

The easy way

Setting nodeIntegration: true in your BrowserWindow gives your renderer process access to node modules. Doing this, is vulnerable. You have access to require("fs") and require("electron"), but that means if someone were to find a XSS vulnerability, they could run any command you’ve exposed in your renderer process.

Think deleting all of the files on your computer, or something else that’s really bad.

The (alternative) easy way

Alongside setting nodeIntegration to true, it’s likely that your app is using webpack to bundle application files. Webpack messes up with certain symbols, so settings like target: 'electron-renderer' or webpack externals allows you to pass through these variables (ipcRenderer) into your app instead.

Still, this changes nothing except how you are setting up your app.

The (other-alternative) easy way

You can use the remote module which gives you access to ipcRenderer. It’s basically ‘The easy way’ in a different form. It’s not recommended by Electron’s security recommendations to do this since this type of attack suffers from a prototype pollution vector.

Ie. using remote could allow someone to modify a js-object’s prototype and wreck havoc on your machine/app.

The almost right way

@marksyzm has a better solution, although not perfect, where we use IPC to send the ipcRenderer to the renderer process. This type of setup is also vulnerable to prototype pollution attacks. If you want to get your app 80% of the way there, I’d use this method, as it probably won’t require you to do much refactoring.

The right way

The right way of importing your fs/ipcRenderer into your renderer process is with IPC (inter-process-communication). This is Electron’s way of allowing you to talk between main and renderer process. Broken down, this is how your app needs to look:

  1. A BrowserWindow has a preload property. This property is a js file that loads with access to require (which means you can require ipcRenderer)
  2. Your BrowserWindow will also have contextIsolation: true to prevent prototype pollution attacks, but this means you need to use the contextBridge to pass the ipcRenderer to your renderer process
  3. Using the preload script and the contextBridge, you allow your renderer process to access the ipcRenderer
  4. In your main script, you create listeners for the ipcRenderer (in the ipcMain module). Within these listeners you can use the fs module

Roughly this is what all these steps look like:

main.js

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

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

async function createWindow() {

  // Create the browser window.
  win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // is default value after Electron v5
      contextIsolation: true, // protect against prototype pollution
      enableRemoteModule: false, // turn off remote
      preload: path.join(__dirname, "preload.js") // use a preload script
    }
  });

  // Load app
  win.loadFile(path.join(__dirname, "dist/index.html"));

  // rest of code..
}

app.on("ready", createWindow);

ipcMain.on("toMain", (event, args) => {
  fs.readFile("path/to/file", (error, data) => {
    // Do something with file contents

    // Send result back to renderer process
    win.webContents.send("fromMain", responseObj);
  });
});

preload.js

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

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        send: (channel, data) => {
            // whitelist channels
            let validChannels = ["toMain"];
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => func(...args));
            }
        }
    }
);

index.html

<!doctype html>
<html lang="en-US">
<head>
    <meta charset="utf-8"/>
    <title>Title</title>
</head>
<body>
    <script>
        window.api.receive("fromMain", (data) => {
            console.log(`Received ${data} from main process`);
        });
        window.api.send("toMain", "some data");
    </script>
</body>
</html>

At the very least, I believe you need electron v7 for these features.

How do I know this?

I care about secure electron apps, and built secure-electron-template in order to create an electron application template to bake-in security instead of thinking of security as an afterthought.

@MarshallOfSound my mistake.

I found the solution in issue #7300 if it can help anyone.

const { ipcRenderer } = window.require('electron');

Please note that this will work when you run the Electron app, but if you just want to test your React code inside the browser it will still crash (window.require is not defined in the browser as it is in Electron).

Right, I have a solution.

  1. Create a preload.js file with the code:
window.ipcRenderer = require('electron').ipcRenderer;
  1. Preload this file in your main.js via webPreferences:
  mainWindow = new BrowserWindow({
    width: 800, 
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      preload: __dirname + '/preload.js'
    }
  });
  1. Now, you will have access from your react app. E.g. this will work:
componentDidMount() {
		if (isElectron()) {
			console.log(window.ipcRenderer);
			window.ipcRenderer.on('pong', (event, arg) => {
				this.setState({ipc: true})
			})
			window.ipcRenderer.send('ping')
		}
	}

Note - using this: https://github.com/cheton/is-electron for the isElectron() function

GitHub issues are for feature requests and bug reports, questions about using Electron should be directed to the community or to the Slack Channel.

CRA uses webpack which messes with standard module loading (including fs).

I’d recommend looking into the Electron mode for webpack and ejecting from CRA

just add target: “electron-renderer” in webpack configs. export default { … target: “electron-renderer” … }

@MarshallOfSound my mistake.

I found the solution in issue #7300 if it can help anyone.

const { ipcRenderer } = window.require('electron');

Please note that this will work when you run the Electron app, but if you just want to test your React code inside the browser it will still crash (window.require is not defined in the browser as it is in Electron).

And for typescript:

import {IpcRenderer} from 'electron';

declare global {
  interface Window {
    require: (module: 'electron') => {
      ipcRenderer: IpcRenderer
    };
  }
}

const { ipcRenderer } = window.require('electron');

If you want to access app.quit(), you can use this:

const { app } = window.require(‘electron’).remote;

Maybe it helps someone…

I’m still getting window.require is not a function. I’m using Electron with React Starter Kit (https://github.com/kriasoft/react-starter-kit). Everything is working nicely, except this.

I’ve set my Electron app to load my app from the web, so the app is not running locally: https://gist.github.com/holgersindbaek/68f6db82f507967a51ca75c527faeff6

What I’m trying to do, is call the ipcRenderer in one of my React files. I’m not sure if it’s even possible when my app is being loaded from the web though. Any suggestions?

Here’s what I did: I used the preload.js trick from above, but in there I placed this code:

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
const { contextBridge, ipcRenderer } = require("electron");

// As an example, here we use the exposeInMainWorld API to expose the IPC renderer 
// to the main window. They'll be accessible at "window.ipcRenderer".
process.once("loaded", () => {
  contextBridge.exposeInMainWorld("ipcRenderer", ipcRenderer);
});

This will make window.ipcRenderer available in the app.

There is, however, one thing I don’t understand. Why can’t Electron just make it bloody work?! How hard can it be to expose some stuff into javascript? Why do we have to jump trough a billion hoops to get foundational functionality working?? I have just spent 2 goddamn hours gettings this to work. I could have have been infinitely more productive without this nonsense.


Oh and I forgot to add, place this in a sensible file somewhere if you’re using Typescript:

import { IpcRenderer } from 'electron';

declare global {
  interface Window {
    ipcRenderer: IpcRenderer
  }
}

Because I am using Typescript, and this makes it typed. Obviously you will be needing to do this for each thing you expose through the context bridge.

Building on @reZach’s comment above, I’m doing something like the following. The main difference is in preload.js, where my API is closer to that of ipcRenderer.

main.js

let newWindow = null;
function createWindow() {
    newWindow = new BrowserWindow({
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            enableRemoteModule: false,
            preload: path.join(__dirname, "preload.js")
        }
    });

    newWindow.webContents.on('did-finish-load', () => {
        newWindow.webContents.send('APP_MY_INIT', { data: 'hi' });
    });
}

ipcMain.on('APP_SOMETHING', (event, ...args) => {
    // ...
});

ipcMain.handle('APP_SOMETHING_ELSE', (event, ...args) => {
    // ...
    return myVar;
});

preload.js

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

function callIpcRenderer(method, channel, ...args) {
    if (typeof channel !== 'string' || !channel.startsWith('APP_')) {
        throw 'Error: IPC channel name not allowed';
    }
    if (['invoke', 'send'].includes(method)) {
        return ipcRenderer[method](channel, ...args);
    }
    if ('on' === method) {
        const listener = args[0];
        if (!listener) throw 'Listener must be provided';
        
        // Wrap the given listener in a new function to avoid exposing
        // the `event` arg to our renderer.
        const wrappedListener = (_event, ...a) => listener(...a);
        ipcRenderer.on(channel, wrappedListener);
        
        // The returned function must not return anything (and NOT
        // return the value from `removeListener()`) to avoid exposing ipcRenderer.
        return () => { ipcRenderer.removeListener(channel, wrappedListener); };
    }
}

contextBridge.exposeInMainWorld(
    'myIpcRenderer', {
        invoke: (...args) => callIpcRenderer('invoke', ...args),
        send: (...args) => callIpcRenderer('send', ...args),
        on: (...args) => callIpcRenderer('on', ...args),
    },
);

client.js

const { myIpcRenderer } = window;

const removeMyListener = myIpcRenderer.on('APP_MY_INIT', data => {
    console.log(data);
    myIpcRenderer.send('APP_SOMETHING', 'foo');
})

async function test() {
    const result = await myIpcRenderer.invoke('APP_SOMETHING_ELSE', 'foo', 'bar');
    console.log(result);
}
test();

if (/* should remove listener === true */) {
    removeMyListener();
}

And for those using TypeScript, types.d.ts

declare global {
    interface Window {
        myIpcRenderer: MyIpcRenderer,
    }
}

export interface MyIpcRenderer {
    invoke(channel: string, ...args: any[]): Promise<any>;
    send(channel: string, ...args: any[]): void;
    
    /** @return A function that removes this listener. */
    on(channel: string, listener: (...args: any[]) => void): () => void;
}

Try disabling ContextIsolation in Electron v12 EDIT: This is considered a security issue, hence, using context isolation is preferred. See context bridge and the Preload API.

for me only work if nodeIntegration is true;

webPreferences: {
      nodeIntegration: true, 
      preload: __dirname + '/preload.js'
}

Bantu aku dengan $1 aku dalam kesusahan uang

Kalau berkenan transfer via rekening mandiri

No. 152-00-1748706-3

Terima kasih orang baik

Pada tanggal Jum, 12 Mar 2021 21.30, dragonDScript @.***> menulis:

Try disabling ContextIsolation in Electron v12

El vie., 12 mar. 2021 1:17, Ivan Maximiliano Saldano < @.***> escribió:

I had to downgrade electron to ^11.3.0 in order for this

const { ipcRenderer } = window.require(‘electron’);

to work.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub <https://github.com/electron/electron/issues/9920#issuecomment-797143298 , or unsubscribe < https://github.com/notifications/unsubscribe-auth/ALANK3H2MU5N5C6WMV3TRVTTDFMRPANCNFSM4DRSRSTQ

.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/electron/electron/issues/9920#issuecomment-797491175, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR7E27ZWSBWZB5PQ2CSMQKDTDIJPTANCNFSM4DRSRSTQ .

I tried all of the above to no avail. What worked for me was a giant hack. Modify the file ./node_modules/electron/index.js and hard code the path to electron.exe

e.g.

function getElectronPath() {
  return 'D:\\Repos\\MyProject\\node_modules\\electron\\dist\\electron.exe';
}

module.exports = getElectronPath();

Im using vuejs and just add the code in vue.config.js

module.exports = { “transpileDependencies”: [ “vuetify” ], pluginOptions: { electronBuilder: { nodeIntegration: true } } }

@genilsonmm Holy crap that worked! I’ve been going crazy for the past 3 hours. Thanks!! The specific piece of that that worked for me was the

   pluginOptions: {
        electronBuilder: {
            nodeIntegration: true
        }
    },

正解,标记

@genilsonmm Holy crap that worked! I’ve been going crazy for the past 3 hours. Thanks!! The specific piece of that that worked for me was the

   pluginOptions: {
        electronBuilder: {
            nodeIntegration: true
        }
    },

For typescript and using @HemalR 's example from above but WITHOUT nodeIntegration: true: https://github.com/electron/electron/issues/9920#issuecomment-336757899:

Right, I have a solution.

  1. Create a preload.js file with the code:
window.ipcRenderer = require('electron').ipcRenderer;
  1. Preload this file in your main.js via webPreferences:
  mainWindow = new BrowserWindow({
    width: 800, 
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      preload: __dirname + '/preload.js'
    }
  });
  1. Now, you will have access from your react app. E.g. this will work:
componentDidMount() {
		if (isElectron()) {
			console.log(window.ipcRenderer);
			window.ipcRenderer.on('pong', (event, arg) => {
				this.setState({ipc: true})
			})
			window.ipcRenderer.send('ping')
		}
	}

Note - using this: https://github.com/cheton/is-electron for the isElectron() function

combined with https://github.com/electron/electron/issues/9920#issuecomment-447157348

I used this:

import { IpcRenderer } from 'electron';

declare global {
  interface Window {
    ipcRenderer: IpcRenderer
  }
}

export const { ipcRenderer } = window;

Hope that helps someone out there! Works with stenciljs, and I imagine react and angular

Here are the steps I used for Vue in-case it’s helpful to someone else.

Ensure node integration is enabled for the renderer process by adding it to the web preferences when creating a browser window:

new BrowserWindow({
    webPreferences: {
        nodeIntegration: true,
    },
})

Configure webpack to package your application for electron renderer by adding a target to your vue.config.js (or wherever you set your vue settings).

module.exports = {
	configureWebpack: {
		target: 'electron-renderer',
	},
}

Import what you need within your application. I’m importing the shell module into my component like so:

import { shell } from 'electron'

@dragonDScript thank you, that did the trick in v12

If you are running your React app in the browser it won’t work. Run it inside Electron and you should be fine.

For anyone new coming to this thread, @reZach has a comprehensive answer with various solutions (from easy to best) that outline what they are and what the vulnerabilities are.

To summarize the security vulnerability: If you are only using your own code, you are probably fine and can use the easy solutions. If you’re importing third-party code (such as jQuery) from a CDN, you could be exposed to XSS attacks.

The above solutions did not work for me.

Note: I am using typescript I managed to work by the following steps:

  1. in preload.ts file
const renderer = window.require('electron').ipcRenderer;

window.addEventListener("DOMContentLoaded", () => {
  window.ipcRenderer = renderer;
  
});

  1. in react app In App.tsx compoent
function App() {
  const [start,setState] = useState(false);
  useEffect(()=>{
    let timer = setInterval(()=>{
      if(!!window.ipcRenderer) {
        clearInterval(timer);
        setState(true);
      }
    },10);
  })
  if(!start){
    return <div></div>;
  }
  return (
    <BrowserRouter>
        <Layout />
    </BrowserRouter>
  );
}

export default App;

@dragonDScript

You should not expose the entire electron api anyway…just the ipc api or remote api (I use ipc).

can you explain why it might be a bad idea to expose the entire electron api? what is the threat model? under which threat model might there be no harm to exposing the entire api?

An attacker could require a node module to attack the computer or the app (in case you use remote content, such as a CDN to import Jquery, etc.).

Electron’s best way is to use a preload script + context bridge api to only expose logic parts of your app. E.g. you don’t:

// DON'T
import {remote} from 'electron'
remote.getCurrentWindow().hide()

// DO
api.hideMainWindow()

// preload.js
// This file has access to all node apis.
import { contextBridge, BrowserWindow } from 'electron'

contextBridge.exposeInMainWorld("api", {
  "hideMainWindow": BrowserWindow.getFocusedWindow().hide(),
})

This also improves speed, because remote and requiring modules in the renderer process is not very fast 😕

I am running create-react-app with TypeScript an Electron. I followed these very good instructions for a setup. But then I also ran into this error. The solution that works for me is the sum of the things said in this thread:

  1. Make react-script use "electron-renderer" as target. I use rescripts with rescript-env for this.

package.json

  "rescripts": [
    "env"
  ],

.rescriptsrc.js

module.exports = [require.resolve(“./webpack.config.js”)];

webpack.config.js:

module.exports = config => {
  config.target = "electron-renderer";
  return config;
};
  1. Add nodeIntegration
new BrowserWindow({
  webPreferences: {
    nodeIntegration: true
  }
});
  1. Add src/typings.d.ts:
declare var window: Window;
interface Window {
  require: any;
}

And theeeen finally in your app

App.tsx

const { remote } = window.require("electron");
console.log(remote.getCurrentWindow());

window.require wasn’t working for me in my main script with error window is not defined, so I switched to const electron = eval('require')("electron"). Hope this helps someone. Using webpack, and problem was that webpack was evaluating my require statement at compilation time.

I had to downgrade electron to ^11.3.0 in order for this

const { ipcRenderer } = window.require('electron');

to work.

@LukasBombach This should work too:

declare var window: Window;
interface Window {
  require: NodeRequire
}

Then you will have typing on the required consts

@moshfeu Your solution works fantastic. I don’t need Webpack or Browserfy to use IpcRenderer in my React project. Thanks so much again 😄

Wow, I couldn’t get the IPCRenderer working on my React Components. I’ve tried all of the method above. Did any of you incidentally have any hints that I can use for it to be working? thanks

just add target: “electron-renderer” in webpack configs. export default { … target: “electron-renderer” … }

Got this working with craco, here’s my craco.config.js:

module.exports = {
    webpack: {
        configure: {
            target: 'electron-renderer'
        }
    }
}

For all the people that have the problem “window.require is not a function”

You have tu create a preoload script on electron.

  1. Create a file named preload.js on the directory where you have de electron main script, Put this code on it:

    window.require = require;

  2. Go to your electron main script and type this in the code where you create the window:

    win = new BrowserWindow({ width: 1280, height: 720, webPreferences: { nodeIntegration: false, preload: __dirname + ‘/preload.js’ },

    }) With this you will preload the script before all, this fixed the problem for me. I hope also for you 😃

To anyone still having the same problem. This is the best solution i have found so far

new BrowserWindow({ 
    webPreferences: {
      nodeIntegration: true
    }
  });

For me worked setting target: “electron-rendering” for webpack and nodeIntegration: true in BrowserWindow options I’m using webpack+electron+react of today’s last versions

process.once("loaded", () => { contextBridge.exposeInMainWorld("ipcRenderer", ipcRenderer); });

@thany thank you very much for this answer. Having pulled hair out with this pile of junk for so long, your solution indeed provided a fix for TS - the contextBridge exposure was the key.

I used https://www.electronforge.io/templates/typescript-+-webpack-template to setup an electron project. None of the solutions above seem to work. Unable to import electron from react.

An insecure solution (https://stackoverflow.com/questions/56091343/typeerror-window-require-is-not-a-function/56095485). I wanted to use typeorm with sqlite without signaling through ipc.

...
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true,
      contextIsolation: false,
      preload: path.resolve('src', 'preload.js')
    }
...
  (global as any).Database = new Database();
...

Database is a class that instantiates a connection to the sqlite database. In essence:

export class Database {
    private connection: Connection;

    constructor() {
        this.init();
    }

    public async init() {
        this.connection = await createConnection({
            type: 'sqlite',
            database: path.resolve('./data.sqlite3'),
            entities: [<Your entities>]
        });
    }

    public get <ENTITY>Repository() {
        return this.connection.getRepository(<ENTITY>);
    }

}

From within react:

const { remote } = window.require('electron');

After which this should produce a valid output:

    useEffect(() => {
        const db: Database = remote.getGlobal("Database");

        const getEntities = async () => {
            const entities = await db.<ENTITY>Repository.find();

            console.log('entities', entities);
        }

        getEntities();
    }, [])

Hope somebody will find it useful.

This is what I came up with today because I need to use BrowserWindow.

webSecurity: true,
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,

I organize my api concerns into topic dedicated modules.

In it, I have my functions to ‘do stuff’. These can be called from preload and from main context.

The renderer process can access it via …

window.api.topic.doStuff(args)

… and the main context (triggered e.g. by context-menu) via a simple:

win.webContents.on('context-menu', (_e, args) => {
  topic.doStuff(args)
})

The topic module itself:

electron.topic.js

'use strict'

import { ipcMain, ipcRenderer, BrowserWindow } from 'electron'

// ----------------------------------------------------------------
// MAIN CONTEXT
// ----------------------------------------------------------------

function init () {
  ipcMain.on('topic:doStuff', (event, args) => {
    doStuff(args)
  })
}

// ----------------------------------------------------------------
// END MAIN CONTEXT
// ----------------------------------------------------------------

// ----------------------------------------------------------------
// PRELOAD CONTEXT
// ----------------------------------------------------------------

function doStuff (args) {
  if (!BrowserWindow) {
    // switch to main context
    ipcRenderer.send('topic:doStuff', args)
    return
  }

  const win = BrowserWindow
    .getFocusedWindow()

  // do stuff with current window
}

// ----------------------------------------------------------------
// END PRELOAD CONTEXT
// ----------------------------------------------------------------

const api = { doStuff }

export default { init, api }

in main I init topic module(s):

main.js

'use strict'

import { app } from 'electron'

import topic from './electron.topic.js'

app.on('ready', async () => {
  topic.init() // <--------- init @ main context

  // Create app window etc...
})

and finally preload script

'use strict'

import { contextBridge } from 'electron'
import topic from './electron.topic.js'

contextBridge.exposeInMainWorld(
  'api',
  {
    platform: process.platform,
    topic: { ...topic.api },
  }
)

@dragonDScript

You could open it directly too, in preload.js. Preload has access to Node functions

I had trouble importing dialog in preload.js it always came out as undefined. The docs also say: https://www.electronjs.org/docs/api/dialog

    Display native system dialogs for opening and saving files, alerting, etc.
Process: Main

so I did this in main:

ipcMain.handle('open-dir', async () => {
  const result = await dialog.showOpenDialog({ properties: ['openDirectory'] })
  return result
})

and this in preload:

import { contextBridge, ipcRenderer } from 'electron';

const apiKey = 'electron';

const api: any = {
  versions: process.versions,
  openDir: () => ipcRenderer.invoke('open-dir'),
};

contextBridge.exposeInMainWorld(apiKey, api);

Still a bit verbose, but it works. Maybe there is another way to do it with node functions.

@dragonDScript

This also improves speed, because remote and requiring modules in the renderer process is not very fast 😕

ok sold. I just read up on remote and its performance implications. So it seems like if we want to open a file dialog prompt for example we need to expose a function that emits an event with the ipcrenderer. Then we act on that in main with opendialog. This seems really verbose. Am I going wrong somewhere? is there an easier way to access main from preload scripts?

You could open it directly too, in preload.js. Preload has access to Node functions

I also got stuck in this issue, trying to follow the contextBridge solution suggested above, to no avail… And then I realized I was starting my Electron app using WSL, not a native terminal (I use Windows) 🤦‍♂️

Just a warning for anyone else that may stumble upon this!

Yes. Now it seems working with:

in main.js

webPreferences: {
  nodeIntegration: false,
  enableRemoteModule: false,
  contextIsolation: true,
  nodeIntegrationInWorker: false,
  nodeIntegrationInSubFrames: false,
  preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},

preload.js :

import { ipcRenderer, contextBridge } from 'electron';

contextBridge.exposeInMainWorld(
  'electron',
  {
    doThing: () => ipcRenderer.send('do-a-thing')
  }
)

index.d.ts :

declare interface Window {
  electron: {
    doThing(): void
  }
}

and renderer.ts :

import './app';

console.log('renderer.js is so happy to say you 👋 hello....');

window.electron.doThing();

@reZach your solution worked for me, but I noticed a mistake that other people might stumble over (because I did, woops!):

        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => fn(...args));
            }
        }

you define your callback function as “func” , but then call “fn”, if you change this, it works precisely as you describe it. Big Thank you for the detailed post 👍

Thanks reZach! I think people will come to understand your approach sooner or later

Mark Elphinstone www.oxfordsourceltd.com

@marc2332 neither are secure. I’m just noting, if you use remote assets, you are opening yourself open to injection attacks this way. 😄

work great the @HemalR solution!

now HOW to send FROM electron TO React?

tried with on electron side

 ipcMain.emit("pong", "Hello!"); 

but nothing got received from the React listener

window.ipcRenderer.on("pong", (event, arg) => {
        console.log("PONG");
});

is correct to use ipcMain.emit() or should I use something else?

@CiriousJoker these is solutions, thanks!