This is a skeleton template for easily creating React-based Electron projects.
It is configured to experience fast development and build speed using Vite bundler. As a bonus, it includes several React utilities and layout configurations.
- β You can build immediately without any presets, so you can develop quickly.
- β
It is being maintained quickly to be compatible with the latest
ReactandElectron, as well as many modules. - β There is no need to worry about layout and data management by using various additional templates.
- β‘οΈ Rapid development through hot-reload
- β‘οΈ Cross-platform development and build support
- β‘οΈ Support for automated application testing
- β‘οΈ TypeScript support
- β‘οΈ Multilingual support
- β‘οΈ Support for themes (dark & light)
- β‘οΈ Basic layout manager
- β‘οΈ Global state management through the Redux store
- β‘οΈ Multi window support through a window manager, switched by a single constant
- β‘οΈ Shared
src/commonfolder for what both processes need, kept process-agnostic by lint rules - β‘οΈ Quick support through the GitHub community
-
For compile & build
viteelectronelectron-builder(Package builder)
-
For web development framework
reactreact-domreact-router-dom@redux/toolkit&react-redux(Global state management)typescript
-
For CSS Design
@mui/material(Material Design CSS Framework)@emotion/react
-
For Multilingual language support
i18next(Multilingual translation)
-
For development utils
eslint(Code syntax checking)eslint-plugin-react-hooksprettier
-
For testing
playwright
- Node.js
22.12.0or later (required byelectron@42andvite@8) - One of
npm,yarnorpnpm
You can easily clone a repository with just the npm command. (Recommend)
$ npm init retronOR, Click Use this template to instantly create your own project.
OR, Clone this repo using below command.
$ git clone https://github.com/jooy2/retron <PROJECT_NAME>Then, install the dependency module.
# via npm
$ npm i
# via yarn (https://yarnpkg.com)
$ yarn install
# via pnpm (https://pnpm.io)
$ pnpm iYou can test your project in the development environment using the following command:
$ npm run dev| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server and launch Electron with hot-reload |
npm run lint |
Report ESLint problems |
npm run lint:fix |
Report ESLint problems and fix the ones that can be fixed automatically |
npm run format |
Check that every file follows the Prettier code style |
npm run format:fix |
Rewrite every file with the Prettier code style |
npm run test |
Build the app and run the Playwright end-to-end suite |
npm run test:linux |
Same as npm run test, wrapped in xvfb-run for headless Linux |
npm run build |
Build a distributable package for the current platform |
npm run build:pre runs on its own before every build target. It type-checks the renderer, the main process, the preload script and the shared code with tsc -b, then bundles the app with Vite.
An Electron app runs in more than one process, and each one has different privileges. Knowing which directory belongs to which process is the first thing to learn about this template.
src
βββ common Code every process shares. No Node.js, no Electron, no React.
β βββ ipc.ts Channel names and the type of `window.mainApi`
β βββ locales.ts Supported languages and their display names
βββ main Main process. Full Node.js access: windows, menus, files, IPC handlers.
β βββ index.ts Application entry point (`main` field of package.json)
β βββ index.dev.ts Development-only extensions, stripped from release builds
β βββ IPCs.ts Every `ipcMain` handler lives here
β βββ constants.ts Shared main process values and feature switches
β βββ security.ts External link and navigation guards
β βββ WindowManager.ts Windows opened on top of the main window
βββ preload Bridge between the two processes. Runs before the page scripts.
β βββ index.ts Exposes `window.mainApi` through `contextBridge`
βββ renderer The React application. Sandboxed, no Node.js access.
β βββ assets Global styles
β βββ components Reusable components
β βββ hooks Reusable hooks
β βββ screens One component per route
β βββ store Redux Toolkit store, slices and pre-typed hooks
β βββ public Static files copied as-is (images, translations)
β βββ i18n.ts i18next setup
β βββ index.html Renderer entry point, including the Content Security Policy
βββ global.d.ts Declares `window.mainApi` for the renderer
buildAssets
βββ builder electron-builder configuration
βββ installer Installer icons
tests Playwright end-to-end suite
The renderer is deliberately unprivileged: nodeIntegration is off and contextIsolation is on. Anything that needs the operating system has to go through IPC.
Some code belongs to neither side: an IPC channel name, a payload type, a validation rule, a pure helper. That goes in src/common, which all three builds import through the same @ alias.
// Same import in src/main, src/preload and src/renderer
import { mainChannels } from '@/common/ipc';Two things to keep in mind when adding your own:
- Only what runs everywhere. The renderer has no Node.js and no Electron, the main process has no DOM and no React. So
src/commonmay not import a Node.js builtin,electronor a renderer library, and may not touchwindow,navigatororprocess. Type-only imports (import type { IpcRendererEvent } from 'electron') are erased at build time and are fine. ESLint fails the build on the rest, so a mistake shows up while you write it rather than at runtime. - Each process gets its own copy. The three bundles are built separately, so a variable exported from
src/commonis not one shared value: changing it in the renderer leaves the main process copy untouched. Keepsrc/commonto constants, types and pure functions, and pass state over IPC.
Channels are whitelisted, so a new one takes two steps. Skipping the first fails fast with Unknown ipc channel name.
1. Declare the channel name in src/common/ipc.ts. mainChannels is Renderer β Main, rendererChannels is Main β Renderer. The preload whitelist is built from these lists, so there is nothing to add there.
export const mainChannels = {
requestGetVersion: 'msgRequestGetVersion',
readConfigFile: 'msgReadConfigFile',
} as const;2. Handle it in the main process in src/main/IPCs.ts.
ipcMain.handle(mainChannels.readConfigFile, async (event, path: string) => readFile(path, 'utf8'));The renderer can then call it, fully typed:
const config = await window.mainApi.invoke(mainChannels.readConfigFile, '/etc/hosts');For the Main β Renderer direction, send from the main process with webContents.send(...) and subscribe with window.mainApi.on(...), which returns the function that removes the listener again. msgNativeThemeUpdated is a working example of this.
Treat every value that arrives from the renderer as untrusted.
openExternalLinkinsrc/main/security.tsshows the expected shape: validate first, act second.
Retron can open extra windows on top of the main window at runtime. They are owned by WindowManager in src/main/WindowManager.ts, and the renderer asks for them over IPC instead of creating them itself.
The feature is switched by FEAT_MULTI_WINDOW in src/main/constants.ts. While it is false, every open request is refused and logged, so multi window support leaves your app with a single constant.
export const FEAT_MULTI_WINDOW = true;Size and placement come from childWindowOptions in the same file.
export const childWindowOptions: ChildWindowOptions = {
width: 720,
height: 540,
maxWindows: 5,
cascadeOffset: { x: 32, y: 32 },
allowDuplicatePath: true,
};| Option | Description |
|---|---|
width / height |
Size of a new window. |
maxWindows |
How many windows may be open at the same time, the main window aside. Requests past the limit are refused and return null. |
cascadeOffset |
{ x, y } offset applied to each new window relative to the window that opened it, so windows do not stack exactly on top of each other. The result stays inside the work area of the same display. |
allowDuplicatePath |
true opens a new window every time. false focuses the window already showing that route instead of opening a second one for it. |
Windows are addressed by route: every window loads the same React app at a different path, so anything reachable in App.tsx can be opened in a window of its own.
import { mainChannels } from '@/common/ipc';
// Resolves with the id of the new window, or `null` when the request was refused
const windowId = await window.mainApi.invoke(mainChannels.openWindow, '/second');
// Closes the window the call is made from. Resolves with `false` in the main
// window, which is never closed this way.
await window.mainApi.invoke(mainChannels.closeWindow);useWindowInfo in src/renderer/hooks reads the state of the current window and keeps it up to date from the msgWindowsUpdated broadcast. A screen shared with the main window should ask it what it is running in rather than assume.
const { isChildWindow, childWindowIds } = useWindowInfo();The example that ships with the template is in MainScreen.tsx and SecondScreen.tsx: the main screen counts the open windows and opens /second in a new one, and the second screen shows a close button when it is running in one of them.
The route comes from the renderer, so it is validated in src/main/security.ts and only plain hash routes such as /second are accepted. New windows get the same webPreferences and navigation guards as the main window, so context isolation and external link handling apply to all of them.
msgCloseWindow only closes windows WindowManager owns, which means a component shared with the main window cannot shut the app down by mistake. Closing the main window closes the rest, so the app never stays alive with windows the user cannot get back from.
Retron can build targeting Windows 10 or later, macOS 12 (Monterey) or later, and major Linux distributions. The macOS floor is the one electron@42 declares; it moves up as Electron drops older releases.
# For Windows (.exe, .appx)
$ npm run build:win
# For macOS (.dmg)
$ npm run build:mac
# For Linux (.rpm, .deb, .snap)
$ npm run build:linuxThe built packages can be found in release/{version} location.
For projects that use the Native Node Module, add the following script to your package.json: When installing dependencies, electron-builder will take care of any modules that require rebuilding.
{
"scripts": {
"postinstall": "electron-builder install-app-deps"
}
}macOS is recommended if you want to build multiple platforms simultaneously on one platform. Because it can be configured with just a few very simple settings.
You can perform multi-platform builds at once with the following command. Alternatively, you can just do it for the OS you want via the individual build commands above.
$ npm run buildAlso check out the Vutron project, which consists of Vite + Vue 3 + Vuetify + Electron.
https://github.com/jooy2/vutron
Anyone can contribute to the project by reporting new issues or submitting a pull request. For more information, please see CONTRIBUTING.md.
Please see the LICENSE file for more information about project owners, usage rights, and more.
