Skip to content

Instantly share code, notes, and snippets.

@Arkellys
Last active May 17, 2024 15:34
Show Gist options
  • Save Arkellys/96359e7ba19e98260856c897bc378606 to your computer and use it in GitHub Desktop.
Save Arkellys/96359e7ba19e98260856c897bc378606 to your computer and use it in GitHub Desktop.
A short guide to answer some of the most common questions and issues on React+Electron apps.

React and Electron, the useful things to know

I like to help folks with their Electron and React problems, but I often have to repeat the same things over and over again. So here is a short guide to some of the most common questions and issues people have.

  1. Read the docs
  2. Adding Electron to an existing React app
  3. Common issues

Read the docs

Yes, I start with this, because Electron's documentation very good and complete, and lot of problems can be solved just by reading it. But since I'm being nice, here is (to me) some important parts:

Quick Start | Inter-Process Communication | Using Preload Scripts | Security | Process Model

Please, read them before asking for help.

Adding Electron to an existing React app

If you have an existing React app and want to wrap it with Electron (i.e. not using a template, boilerplate or special tool), you can start by reading the instructions of the Quick Start guide. You don't have to create a new app, but you will need these:

Setup with React

When creating a BrowserWindow, you can either load an URL (loadURL) or a File (loadFile) into this window. And when developing a React app, you either have a development server (URL), or a production build (File).

So, how do you connect Electron and React?

In development you have to start your React dev server, wait for it to be ready and then start Electron with your BrowserWindow loading the server URL (localhost). In production, you have to build your React app and make your BrowserWindow load the built HTML file. To start Electron in production, you have to package it into a real app.

Here is how it looks with code (paths and names depend on your config):

app.isPackaged
  ? mainWindow.loadFile(path.join(__dirname, "index.html")) // Prod
  : mainWindow.loadURL("http://localhost:3000"); // Dev

Wait for my dev server to be ready?

However you created your React app in the first place, on your package.json you must have a script to run your app in dev (start, dev, serve... the name depends on how your app was configured). Now that you have a new script to start electron, you may want to add another script so you can run React and Electron with a single command line.

For this, you can use the modules wait-on (to wait for the server to be ready) and concurrently (to run multiple scripts). Here is an example for a React app created with CRA:

"scripts": {
  "start:react": "react-scripts start",
  "start:electron": "electron .",
  "start": "concurrently \"yarn start:react\" \"wait-on http://localhost:3000/ && yarn start:electron\"",
}
yarn start

If you don't use Yarn, replace yarn with npm.

Packaging for production

To package your app for production, you can start by reading the application packaging docs.

Whether you decide to use Electron Forge, electron-builder or Electron Packager (if you feel confident), you will want to package only what you need. In the context of a React app, it means you will need to first build your React app (with whatever bundler you use), and then only package the bundled files. Note that some dependencies, such as native modules, cannot be bundled, so you will need to configure your packager to handle them separately from your bundled files.

For Electron Forge (which use Electron Packager under the hood), you can only specify the files you don't want to package, using the ignore option. Here is an example of configuration (you can also use an ignoreFunction):

packagerConfig: {
  ignore: [
    "^\\/public$",
    "^\\/src$",
    "^\\/node_modules$",
    "^\\/[.].+",
    // ...
  ]
},

For electron-builder, you can specify the files you want to package with the files option:

{
  files: ["./build/**/*"],
  // ...
}

Of course, this configuration entirely depends on your file and folder structure. Here is my personal advice though: to prevent conflicts, make sure your build folder (for React) has a different name than your packaging folder (for Electron).

Common issues

The issues below are not always specific to React+Electron apps, but they are so common that I prefer to include them.

  1. It doesn't work, and there is no error
  2. Error when using require
  3. Cannot find module 'fs'
  4. Can't load files/modules into preload
  5. Blank page in production
  6. Images/assets not displayed with Vite in production
  7. Application entry file does not exist
  8. __dirname is not defined

Have you tried to look into the developer console?

No joke, people doing front-end development with the console closed is far too common. When working with Electron, remember you have two consoles: the one for your main process (i.e. the one you used to start your app) and the one for your renderer (i.e. the browser's). Look for errors into both of them.

You can open the DevTools from the menu or your app or using the shortcut Ctrl+Shift+I. You can also configure your window to open them automatically:

mainWindow.webContents.openDevTools();

Problems with require will give you various types of errors, such as:

- Uncaught ReferenceError: require is not defined
- TypeError: windows.require is not a function
- Uncaught Error: Dynamic require of "xxx" is not supported

You will find a lot of questions about this on Stack Overflow, and sadly a lot of accepted answers will just tell you to disable security, which is bad (unless you know what you're doing).

Electron comes with several security features that are all enabled by default. These features will stop you from using require in your renderer process (i.e. your React code). It means that, by default, your BrowserWindow will have this configuration:

const mainWindow = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    webSecurity: true
  }
});

If you care about security, you want to keep it this way.

Fortunately, you don't need to disable security to use Node.js APIs, you can use IPC and a preload file. You will find a lot of nice examples in the docs linked. If you have difficulties to understand how IPC and preload work, I also suggest you take a look at this very good article.

- Uncaught Error: Cannot find module 'fs'
- Error: Can't resolve 'fs' in 'xxx'

This error happens when you try to compile Node.js code with webpack, and that it is not configured for it. This usually means you require/import something from electron, or try to use Node.js APIs in your renderer process (i.e. your React code). Doing this is a security risk.

To solve this issue, you can follow the same instructions as for common issue #2. If you can't find where the bad import is in your code, also make sure you are not importing your main or your preload file into your renderer.

If you get this error when trying to bundle your main and/or preload files, then it means you need to configure webpack with the correct target: electron-main or electron-preload. Note that the target electron-renderer also exists.

You followed the IPC tutorial, implemented a contextBridge on your preload file, tried to use an exposed function on your renderer, but you get this error:

- Error: Cannot read properties of undefined (reading xxx)

Or maybe you get this:

- Unable to load preload script: xxx
- Error: module not found: xxx

This is because for security reasons, Electron's processes are sandboxed by default. For the preload, it means that you can only require some specific modules (emphasis mine):

In order to allow renderer processes to communicate with the main process, preload scripts attached to sandboxed renderers will still have a polyfilled subset of Node.js APIs available. A require function similar to Node's require module is exposed, but can only import a subset of Electron and Node's built-in modules: [...]

If you're only looking to split your preload into multiple files, you should be able to do so with your bundler (and then use import instead of require). If what you want is to use Node.js APIs not included in the subset, you will need to move these on your main process and use IPC to get what you need.

If for whatever reason you don't want to move everything on main, another possibility is to disable sandboxing. But don't forget that it is a security feature which is not recommended to disable.

const mainWindow = new BrowserWindow({
  webPreferences: {
+   sandbox: false,
    preload: path.join(__dirname, 'preload.js')
  }
});

The first thing you should check is that your HTML file is correcly loaded. If your HTML is here and you don't have any error but your page is empty, most of the time it's because you are not using the correct router or that you've forgotten something in your configuration (or both).

When using React Router with Electron, you should use hash routing. It means either <HashRouter> or createHashRouter. Don't worry if you were using <BrowserRouter>/createBrowserRouter, you can just replace it and it will work, no further code changes are required.

root.render(
- <BrowserRouter>
+ <HashRouter>
    {/* ... */}
- </BrowserRouter>,
+ </HashRouter>,
  root
);

Note that it should also work with <MemoryRouter>/createMemoryRouter as long as you don't need to navigate on a page using loadURL/loadFile.

Another reason for the blank page can be a missing configuration. Depending on the tool you use to run React, you may need to specify the base path to serve your app. For example, if you used CRA to create your app, you have to add a homepage field on your package.json:

{
  "homepage": "./",
  // ...
}

For Vite, see common issues #6. If you use another tool, you will have to check its documentation to see if a similar option is required.

If you use Vite for your React app, in order to load assets correctly in production on an Electron app you need to add the base option on your configuration file:

export default defineConfig({
  plugins: [react()],
+ base: "./",
  // ...
});
- Error: Application entry file "build/electron.js" in the "xxx" does not exist. Seems like a wrong configuration.

This electron-builder error is specific to React app created with CRA. Electron-builder as a built-in preset for CRA that it will apply by default when detecting react-scripts. This preset expects your entry file to be named electron.js and to be in the build folder when packaging. You can either decide to respect the expected structure, or you can disable the preset using the extends option on your configuration file.

extends Array<String> | String | “undefined” - The name of a built-in configuration preset (currently, only react-cra is supported) or any number of paths to config files (relative to project dir).

You can disable extends by passing null:

module.exports = {
+ extends: null,
  // ...
};
- Uncaught ReferenceError: __dirname is not defined

If you get this error, you are probably trying to use electron APIs on your renderer process (i.e. your React code). The variable __dirname is a Node.js global, which require access to the Node.js APIs to be used. And for security reasons, access to Node.js from your renderer is disabled by default.

To solve this issue, you can follow the same instructions as for common issue #2. If you can't find where the bad import is in your code, also make sure you are not importing your main or your preload file into your renderer.


You still have an issue that you can't solve?

Try to search on Stack Overflow and Electron's issues to see if anyone else has had the same problem as you. You can also join Electron's official Discord server and seek for help here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment