Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Vite and React

An Electron page is an ordinary web page, so it can be written in React, Vue or another framework. This needs a bundler that turns components into JavaScript for the browser. Electron Forge can create a project with the Vite bundler: it builds the page, the main process and preload.

Creating the project

npm init electron-app@latest notes-react -- --template=vite
cd notes-react
npm start

For TypeScript there is the vite-typescript template. After npm start Vite runs a dev server, and Electron opens a window that loads the page from it.

Structure

notes-react/
├── src/
│   ├── main.js        the main process
│   ├── preload.js     the preload script
│   ├── renderer.js    page code
│   └── index.css
├── index.html
├── forge.config.js
├── vite.main.config.mjs
├── vite.preload.config.mjs
└── vite.renderer.config.mjs

Each part has its own Vite config file. In this template the main process uses import instead of require: Vite bundles it into a single CommonJS file anyway. Within one project, stick to one style.

Loading the page

In src/main.js the window loads the page like this:

// src/main.js
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
  mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
  mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
}

Forge fills in the constants. During development the window opens the Vite server address, so component changes show up at once. In the built app the finished file is loaded.

Adding React

npm install react react-dom
npm install --save-dev @vitejs/plugin-react

Add the plugin to the page build config:

// vite.renderer.config.mjs
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [react()],
});

Rename src/renderer.js to src/renderer.jsx, fix its path in the script tag in index.html, and add a <div id="root"></div> element to body.

A component

// src/renderer.jsx
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
 
createRoot(document.getElementById('root')).render(<App />);
// src/App.jsx
import { useEffect, useState } from 'react';
 
export default function App() {
  const [notes, setNotes] = useState([]);
 
  useEffect(() => {
    window.notes.load().then(setNotes);
  }, []);
 
  return (
    <ul>
      {notes.map((note, i) => <li key={i}>{note}</li>)}
    </ul>
  );
}

The component gets notes through the same window.notes as before: IPC handlers go into src/main.js, and the bridge into src/preload.js. After npm start the list appears in the window.

Vue and other options

For Vue, install vue and @vitejs/plugin-vue and add the vue() plugin in the same file. There is also a separate tool, electron-vite, with templates for React, Vue and Svelte — it works the same way.

Common mistake

Importing fs or path in a React component does not give file access: page code is built for the browser, and Vite either fails or substitutes an empty stub. React is only the renderer process. Keep everything that needs Node.js in the main process and call it through preload.

Thanks for reading!

Donate to the site — we work hard for you.