DEV Community

Cover image for Multi-Page Electron Apps Exist β€” Even If We Rarely Talk About Them
Nandani Bansal
Nandani Bansal

Posted on

Multi-Page Electron Apps Exist β€” Even If We Rarely Talk About Them

Multi-Window Navigation in Electron: The Complete Guide πŸš€

If you've ever built an Electron desktop application with multiple pages or windows, you know the challenge: managing navigation that works smoothly in both development and production can be surprisingly complex. Today, I'm sharing practical solutions to this common problem.

The Challenge We All Face

When building desktop apps with Electron, handling multiple pages isn't as straightforward as web applications. You encounter issues like:

  • Different behavior in dev vs production builds
  • Window state management across page transitions
  • File protocol limitations breaking traditional routing
  • Complex window lifecycle management

Let me show you battle-tested approaches that actually work.

Key Features You Need

Any solid multi-page Electron solution should provide:

  • Consistent behavior in development and production
  • Multiple independent windows with isolated navigation
  • Persistent window states and positions
  • Fast page transitions without flickering
  • Type-safe navigation (if using TypeScript)

Installation & Setup

First, let's set up the foundation. In your terminal:

npm install electron react react-dom
# If using React Router
npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Implementation Approaches

Approach 1: File-Based Multi-Page

This works great for apps with distinct, independent pages.

** main.js:**

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createMainWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  });


  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:3000');
  } else {
    mainWindow.loadFile(path.join(__dirname, '../build/index.html'));
  }

  return mainWindow;
}

function createSettingsWindow() {
  const settingsWindow = new BrowserWindow({
    width: 800,
    height: 600,
    parent: mainWindow,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });

  if (process.env.NODE_ENV === 'development') {
    settingsWindow.loadURL('http://localhost:3000/#/settings');
  } else {
    settingsWindow.loadFile(
      path.join(__dirname, '../build/index.html'),
      { hash: 'settings' }
    );
  }

  return settingsWindow;
}
Enter fullscreen mode Exit fullscreen mode

Approach 2: Single Window with Dynamic Routes

Better for apps with frequent navigation between pages.

App Structure:

import { MemoryRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <MemoryRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/settings" element={<SettingsPage />} />
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </MemoryRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why MemoryRouter? It keeps navigation in memory rather than the URL bar, perfect for desktop apps.

Window Management Best Practices

Persistent Window States:

const Store = require('electron-store');
const store = new Store();

function createWindowWithState(windowId) {
  const savedState = store.get(`window.${windowId}`, {
    width: 800,
    height: 600,
    x: undefined,
    y: undefined
  });

  const window = new BrowserWindow({
    ...savedState,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });

  // Save state on close
  window.on('close', () => {
    store.set(`window.${windowId}`, window.getBounds());
  });

  return window;
}
Enter fullscreen mode Exit fullscreen mode

Prevent Duplicate Windows:

const activeWindows = new Map();

function openWindow(windowId, options) {
  // Return existing window if already open
  if (activeWindows.has(windowId)) {
    const window = activeWindows.get(windowId);
    window.focus();
    return window;
  }

  const window = new BrowserWindow(options);
  activeWindows.set(windowId, window);

  window.on('closed', () => {
    activeWindows.delete(windowId);
  });

  return window;
}
Enter fullscreen mode Exit fullscreen mode

Inter-Window Communication

Preload Script:

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  openWindow: (windowId) => ipcRenderer.send('open-window', windowId),
  sendToWindow: (windowId, data) => 
    ipcRenderer.send('send-to-window', windowId, data),
  onMessage: (callback) => 
    ipcRenderer.on('window-message', (_, data) => callback(data))
});
Enter fullscreen mode Exit fullscreen mode

Main Process Handler:

ipcMain.on('send-to-window', (event, targetWindowId, data) => {
  const targetWindow = activeWindows.get(targetWindowId);
  if (targetWindow) {
    targetWindow.webContents.send('window-message', data);
  }
});
Enter fullscreen mode Exit fullscreen mode

Navigation Component

Create a reusable navigation component:

function Navigation() {
  const navigate = useNavigate();

  const handleOpenSettings = () => {
    window.electronAPI.openWindow('settings');
  };

  return (
    <nav className="app-nav">
      <button onClick={() => navigate('/')}>Home</button>
      <button onClick={() => navigate('/profile')}>Profile</button>
      <button onClick={handleOpenSettings}>Settings (New Window)</button>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Production Build Configuration

package.json scripts:

{
  "scripts": {
    "dev": "concurrently \"npm run dev:react\" \"npm run dev:electron\"",
    "dev:react": "vite",
    "dev:electron": "wait-on http://localhost:3000 && electron .",
    "build": "vite build && electron-builder"
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls to Avoid

  1. Don't use BrowserRouter - It relies on URL changes which don't work well with the file protocol
  2. Always handle both dev and production - Paths differ significantly
  3. Never forget contextIsolation - It's a critical security feature
  4. Avoid memory leaks - Always remove event listeners on window close
  5. Test production builds - Dev mode can hide issues

Performance Tips

// Lazy load heavy components
const SettingsPage = lazy(() => import('./pages/Settings'));

// Preload next likely page
const preloadPage = (path) => {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = path;
  document.head.appendChild(link);
};
Enter fullscreen mode Exit fullscreen mode

Full Example Structure

my-electron-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”œβ”€β”€ main.js
β”‚   β”‚   └── preload.js
β”‚   β”œβ”€β”€ renderer/
β”‚   β”‚   β”œβ”€β”€ App.jsx
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ Home.jsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Settings.jsx
β”‚   β”‚   β”‚   └── Profile.jsx
β”‚   β”‚   └── components/
β”‚   β”‚       └── Navigation.jsx
β”‚   └── shared/
β”‚       └── types.ts
β”œβ”€β”€ package.json
└── electron-builder.json
Enter fullscreen mode Exit fullscreen mode

Ready to Build?

With these patterns, you can build desktop applications that handle multi-page navigation elegantly. The key is understanding the difference between web and desktop environments and leveraging Electron's unique capabilities.

Top comments (0)