ipcRenderer in CRA managed app
For my first electron app with react i’ve used CRA.
What is kinda comfy to start, but pretty soon I’ve faced a problem that react scripts cannot properly pack ipcRender from electron package.
I’ve blamed CRA at the beginning, however some research I’ve found that neither parcel, neither webpack won’t solve the problem in a simple and transparent way.
The problem is — if you try to import ipcRender in web application, it crashes with TypeError: fs.existsSync is not a function.
To be more precise this ↓
import { ipcRenderer } = from 'electron'will end-up like this ↓

Without deep sh*t — it cannot be packed, it should be requested by old good require.
There are several solutions that, described below, mind the one that suits you more and all of them are pretty simple.
An easy but vulnerable approach
In ./public/electron.js enable nodeIntegration which will allow use of node packages in renderer process.
Mind this is very VULNERABLE approach, and described just for educational purposes. To be clear, with
nodeIntegrationenabled, the successful XSS attack will provide the malefactor with all the power of NodeJS under privileges of user running the app.
mainWindow = new BrowserWindow({
  width: 800, height: 600,
  webPreferences: {
    nodeIntegration: true
  }
})CRA uses webpack,
and webpack has __non_webpack_require__ function that behaves like classic require instead of prepacking the specified module.
Fo reuse across react app let’s put it in a separate module say ./src/appRuntime.ts.
import { IpcRenderer } from 'electron' // this is just an interface
declare var __non_webpack_require__: (id: string) => any
const electron = __non_webpack_require__('electron')
export const ipcRenderer: IpcRenderer = electron.ipcRendererFurther in react app, just import it from ./src/appRuntime.ts not from electron.
For example in ./src/App.tsx
import { ipcRenderer } from './appRuntime'Vulnerable approach example @ github
A (kind of) fine approach
First thing is to disable nodeIntegration,
and specify path to preload script, in ./public/electron.js
mainWindow = new BrowserWindow({
  width: 800, height: 600 ,
  webPreferences: {
    nodeIntegration: false,
    preload: path.resolve(__dirname, './preload.js')
  }
})As nodeIntegration is disabled, the classic require function won’t work within rederer context anymore,
however it is still available in preload script context.
Therefore we can resolve electron using classic require and store ipcRenderer in a global variable in ./public/preload.js.
const { ipcRenderer } = require('electron')
window.ipcRenderer = ipcRendererIn react app we can expect ipcRender to be available in a global context,
for more convenient use across the app, can use separate module like ./src/appRuntime.ts
import { IpcRenderer } from 'electron' // this is just an interface
export const ipcRenderer: IpcRenderer = (window as any).ipcRendererFinally, same as in previous approach, further in react app,
just import it from ./src/appRuntime.ts not from electron.
For example in ./src/App.tsx.
import { ipcRenderer } from './appRuntime'As renderer process does not have access to require node modules,
this is less vulnerable as previous approach and quite a popular approach.
However even tho it might seem harder to exploit,
the hybrid context is still vulnerable to prototype pollution attach
that once again can give the malefactor full control of a NodeJS process.
A (kind of) fine approach example @ github
The ‘proper’ and secure approach
As hybrid context is a vulnerability, it can be switched off by enabling contextIsolation,
obviously the nodeIntegration should be disabled as well, and we’ll still gonna need the
preload script to expose some APIs to the renderer, in ./public/electron.js.
mainWindow = new BrowserWindow({
  width: 800, height: 600,
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    preload: path.join(__dirname, "preload.js")
  }
})With contextIsolation being enabled,
the preload script will still have access to node modules,
it will isolated from main renderer process so won’t share global variables as well.
Therefore there is a contextBridge to expose APIs we need to renderer in ./public/preload.js
const {
    contextBridge,
    ipcRenderer
} = require("electron");
contextBridge.exposeInMainWorld(
    "appRuntime", {
        send: (channel, data) => {
            ipcRenderer.send(channel, data);
        },
        subscribe: (channel, listener) => {
            const subscription = (event, ...args) => listener(...args);
            ipcRenderer.on(channel, subscription);
            return () => {
                ipcRenderer.removeListener(channel, subscription);
            }
        }
    }
)The exposeInMainWorld function takes two arguments.
First apiKey will be the name global variable, under which, the exposed api will be available in renderer.
Second api the object to be exposed.
Also notice that in subscribe, the listener is not being passed directly to ipcRenderer, it’s wrapped in function that prevents leak of ipcRednerer’s event object to a renderer process.
In react app we can expect that exposed object to be available under global variable appRuntime, as we named it in exposeInMainWorld,
and what’s left is just to add some typings let’s say in ./src/appRuntime.ts
type Unsubscribe = () => void;
type Listener = (...args: any[]) => void;
interface AppRuntime {
    send: (channel: string, data: any) => void
    subscribe: (channel: string, listener: Listener) => Unsubscribe
}
const appRuntime = (window as any).appRuntime as AppRuntime
export default appRuntimeMind that everything you are exposing to renderer process is vulnerable to XSS attacks. So keep it simple — primitives in, primitives out.
Further in react app, just import and use the exported api, for example in ./src/App.tsx
import appRuntime from './appRuntime';The secure approach example @ github
Good luck, with your great app :)
