Is it possible to make an electron to only be
instantiated once?
Yes, in Electron.js, it's very common to want the application to only have one instance running at a time (e.g. to avoid opening multiple windows if the user tries to launch the app again). Electron provides a built-in way to do this using the app.requestSingleInstanceLock()
API.
javascriptconst { app, BrowserWindow } = require('electron'); let mainWindow; // Try to get the single instance lock const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { // Another instance is already running β quit this one app.quit(); } else { app.on('second-instance', (event, commandLine, workingDirectory) => { // Someone tried to run a second instance β focus the main window if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); // Create the main window app.whenReady().then(() => { mainWindow = new BrowserWindow({ width: 800, height: 600, }); mainWindow.loadURL('https://example.com'); mainWindow.on('closed', () => { mainWindow = null; }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); }
app.requestSingleInstanceLock()
returns true
if the current instance is the first one. If it's false
, another instance is already running.
The second-instance
event is emitted in the first instance when a second instance is started. This is where you can focus or restore the main window.
The second instance simply exits with app.quit()
.
This only works in packaged apps or when running from the same app path.
On macOS, apps behave slightly differently β often keeping the app running even when no windows are open. Handle that with app.on('activate', ...)
.
Let me know if you're using a specific Electron version or need this to work with specific behaviors like protocol handling or deep linking.
To use React in an Electron app, you can combine the two by:
Using Electron for the backend (main process and window management).
Using React for the frontend (rendered in a BrowserWindow
via a web page).
You can set this up manually or use tools like Vite, Webpack, or Create React App. Below is a basic setup using Vite + Electron, which is modern and fast.
my-electron-app/ ββ main/ β Electron Main process code β ββ main.js ββ renderer/ β React code (Vite project) β ββ index.html β ββ main.jsx β ββ ... ββ package.json
bashmkdir my-electron-app cd my-electron-app npm init -y
bashnpm install electron --save-dev npm create vite@latest renderer -- --template react cd renderer npm install cd ..
main.js
(Electron entry point)
Create a main/main.js
:
jsconst { app, BrowserWindow } = require('electron'); const path = require('path'); function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), // optional }, }); win.loadURL('http://localhost:5173'); // Vite dev server } app.whenReady().then(() => { createWindow(); });
start
script in package.json
json"scripts": { "dev": "concurrently \"npm:dev:*\"", "dev:react": "cd renderer && vite", "dev:electron": "electron main/main.js" }, "devDependencies": { "concurrently": "^8.0.0" }
Install concurrently:
bashnpm install concurrently --save-dev
bashnpm run dev
You should see a window open with your React app inside.
To build the app for production:
Build React:
bashcd renderer npm run build cd ..
Update main.js
to load the built HTML:
jswin.loadFile(path.join(__dirname, '../renderer/dist/index.html'));
Use tools like electron-builder
to package the final app.
Component | Role |
---|---|
Electron | Manages windows and system APIs |
React |
Builds the UI (in BrowserWindow )
|
Vite | Fast frontend dev/build tool |
Let me know if you'd prefer using Create React App, Webpack, or want a fully packaged Electron + React template, and I can generate that too.
To handle routes in your Electron + React app, you use React Router just like in any other React web app. However, since Electron apps are typically single-page applications (SPA) running locally, there are a few extra considerations to ensure routing works smoothly in development and production.
In your React frontend (e.g., renderer/
):
bashnpm install react-router-dom
In your renderer/src/main.jsx
(or App.jsx
):
jsximport React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter as Router, Routes, Route, } from 'react-router-dom'; import Home from './pages/Home'; import About from './pages/About'; const App = () => ( <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Router> ); ReactDOM.createRoot(document.getElementById('root')).render(<App />);
Everything should work as expected β routing is handled by React Router using the browser's history API.
When running vite
(npm run dev
), Electron loads from:
jswin.loadURL('http://localhost:5173');
React Router works like a regular web app.
file://
)
When Electron loads your built app from file://
, BrowserRouter
can fail to handle deep links like /about
. This is because there's no real server to respond with your index.html
.
HashRouter
instead
In App.jsx
or main.jsx
, replace:
jsximport { BrowserRouter } from 'react-router-dom';
with:
jsximport { HashRouter } from 'react-router-dom';
And change:
jsx<BrowserRouter>
to:
jsx<HashRouter>
HashRouter
?
URLs look like app://index.html#/about
instead of app://index.html/about
.
Works out of the box with local file://
URLs.
Avoids 404 errors on refresh or direct navigation.
You can still use dynamic routes:
jsx<Route path="/user/:id" element={<UserPage />} />
And get the id
with useParams()
in your component:
jsximport { useParams } from 'react-router-dom'; const { id } = useParams();
Mode | Router to Use | Notes |
---|---|---|
Development |
BrowserRouter
|
Works fine with Vite + dev server |
Production |
HashRouter
|
Safe with Electron loading file:// HTML
|
Let me know if you want code examples for navigation menus or linking between routes!
Great question β you're looking to build a hybrid routing setup in an Electron + React app, where:
Some pages are loaded directly by Electron (via BrowserWindow.loadURL()
or loadFile()
), and
Other pages are handled within React using react-router-dom
.
This can work well if you want separate windows or need native logic separation (e.g., login screen in one window, dashboard in another).
You can combine Electron routing (main process) with React Router (renderer process):
Electron decides which window to show (e.g. login, dashboard).
React Router controls navigation within that window.
BrowserWindow.loadURL()
to Route from Electron
js// main/main.js const { app, BrowserWindow } = require('electron'); const path = require('path'); function createLoginWindow() { const win = new BrowserWindow({ width: 500, height: 400 }); win.loadURL('http://localhost:5173/#/login'); // use hash route } function createDashboardWindow() { const win = new BrowserWindow({ width: 1000, height: 700 }); win.loadURL('http://localhost:5173/#/dashboard'); } app.whenReady().then(() => { // Example: open login screen first createLoginWindow(); });
π‘ In production, use
win.loadFile('dist/index.html', { hash: '#/login' })
.
HashRouter
in React (so deep links work with file://)
jsximport { HashRouter, Routes, Route } from 'react-router-dom'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; function App() { return ( <HashRouter> <Routes> <Route path="/login" element={<Login />} /> <Route path="/dashboard/*" element={<Dashboard />} /> </Routes> </HashRouter> ); }
You can navigate between /dashboard/home
, /dashboard/settings
, etc. β React Router handles that normally inside the dashboard window.
jsx<Route path="/dashboard/home" element={<Home />} /> <Route path="/dashboard/settings" element={<Settings />} />
Electron Window | Route You Load | React Pages Inside |
---|---|---|
Login |
#/login
|
Only login screen |
Main App |
#/dashboard
|
Dashboard, Settings, etc |
Popup |
#/popup/settings
|
Small settings view |
Use HashRouter
to avoid issues with file://
and deep linking.
Avoid switching windows within React Router β Electron should create/destroy windows.
You can send data between windows via ipcRenderer
/ ipcMain
if needed.
You can send a message from React to Electron to open a new window:
js// In renderer window.electronAPI.openDashboard()
js// In preload.js contextBridge.exposeInMainWorld('electronAPI', { openDashboard: () => ipcRenderer.send('open-dashboard') }); // In main.js ipcMain.on('open-dashboard', () => { createDashboardWindow(); });
Let me know if you want a complete working starter with Electron, Vite, React, and hybrid routing β I can generate the template for you.
Great question! A hash route is a way to implement routing in web applications that works without server configuration, which is why it's commonly used in Electron apps and static deployments.
A hash route uses the #
symbol in the URL to simulate navigation.
http://localhost:5173/#/dashboard
Here:
The browser loads index.html
from localhost:5173
The part after #
(i.e., #/dashboard
) is not sent to the server
React Router (or your frontend router) reads the hash and decides what to render
Browsers treat anything after a #
in a URL as a fragment identifier, which is:
Not sent to the server
Handled entirely on the client side (JavaScript)
This is ideal in Electron and static site hosting, where:
Thereβs no backend server to respond with different pages
Everything is served from one index.html
HashRouter
vs BrowserRouter
(React Router)
Router | URL Format |
Works with file:// or static servers
|
Requires server config |
---|---|---|---|
HashRouter
|
http://localhost/#/about
|
β Yes | β No |
BrowserRouter
|
http://localhost/about
|
β No (breaks with file://) | β Yes (needs server) |
Electron loads files using:
jswin.loadFile('dist/index.html');
This uses the file://
protocol, which:
Doesnβt support clean URLs like /about
Will break if you use BrowserRouter
and refresh
β
HashRouter
avoids this because:
It never touches the actual file path
Routing happens completely inside your React app
Hash route: a route using #
, like #/settings
Handled completely in the browser
Works perfectly in Electron, static sites, and file:// contexts
Use HashRouter
from react-router-dom
to make it work
Let me know if you'd like to see a working code example using HashRouter
vs BrowserRouter
.