Getting Started
Install
Section titled “Install”npm install @loc/electron-windowHow it works
Section titled “How it works”<Window open> in your React tree calls window.open(). The main process intercepts that via setWindowOpenHandler, creates a BrowserWindow with the props you pre-registered over IPC, and hands back a blank document. React then portals your children into that document — so they stay in the parent React tree (your context providers keep working), but their DOM lives in the child window.
Understanding this helps with the edge cases: window in your component code is the parent renderer’s window (React runs there), but element.ownerDocument is the child’s document (DOM lives there). See Patterns → Integration Notes when you hit this.
Three files — one per Electron process.
Main process
Section titled “Main process”import path from "node:path";import { app, BrowserWindow } from "electron";import { setupWindowManager } from "@loc/electron-window/main";
const manager = setupWindowManager({ defaultWindowOptions: { webPreferences: { preload: path.join(__dirname, "preload.js"), }, },});
app.whenReady().then(() => { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, "preload.js") }, }); manager.setupForWindow(mainWindow); mainWindow.loadFile("index.html");});setupWindowManager is the only place where webPreferences can be configured. The renderer cannot override them. See Security for details.
setupForWindow accepts a BrowserWindow, WebContentsView, or bare WebContents — useful if your parent renderer is a WebContentsView rather than a top-level window.
Preload script
Section titled “Preload script”// preload.ts — must be bundled (esbuild, webpack, etc.)import "@loc/electron-window/preload";This sets up the IPC bridge between the main and renderer processes.
Renderer
Section titled “Renderer”// rendererimport { useState } from "react";import { WindowProvider, Window } from "@loc/electron-window";
function App() { const [showSettings, setShowSettings] = useState(false);
return ( <WindowProvider> <button onClick={() => setShowSettings(true)}>Settings</button> <Window open={showSettings} onUserClose={() => setShowSettings(false)} title="Settings" defaultWidth={600} defaultHeight={400} > <SettingsPanel /> </Window> </WindowProvider> );}WindowProvider must wrap all <Window> usage. Children of <Window> live in the parent React tree — Redux stores, theme providers, routers, and context all work inside child windows without extra wiring.
<WindowProvider> props
Section titled “<WindowProvider> props”| Prop | Type | Default | Description |
|---|---|---|---|
devWarnings | boolean | true when NODE_ENV==="development" | Enable dev warnings. Set explicitly in sandboxed renderers where process.env isn’t available. |
debug | boolean | false | Log every IPC call and event to the console. |
In a sandboxed renderer (the library’s default), process.env.NODE_ENV is undefined — dev warnings won’t fire unless you set <WindowProvider devWarnings> explicitly.
Allowing external links
Section titled “Allowing external links”By default, the library denies any window.open() from the parent renderer that isn’t an about:blank managed window. This means <a target="_blank"> silently fails. To route external URLs to the system browser, pass a fallback handler:
import { shell } from "electron";
manager.setupForWindow(mainWindow, { fallbackWindowOpenHandler: ({ url }) => { if (url.startsWith("https://")) shell.openExternal(url); return { action: "deny" }; },});Without this, a dev warning fires: "window.open to <url> denied. Not a managed window and no fallbackWindowOpenHandler provided."
Main-process API
Section titled “Main-process API”setupWindowManager returns (and getWindowManager() retrieves) a WindowManager instance. Useful for tray menus, “close all windows”, or anything else in the main process that needs to touch child windows:
import { getWindowManager } from "@loc/electron-window/main";
const manager = getWindowManager();
// All managed child windowsfor (const win of manager.getAllWindows()) { win.focus(); // or .close(), .minimize(), .destroy(), ... const state = win.getState(); // { id, bounds, isFocused, isVisible, ... }}
// One window by IDconst instance = manager.getWindow(someId);instance?.window?.setBounds({ width: 800, height: 600 }); // raw BrowserWindowWindowInstance wraps a BrowserWindow with the library’s lifecycle methods. instance.window gives you the underlying Electron object if you need something the wrapper doesn’t expose.
Troubleshooting
Section titled “Troubleshooting”Window doesn’t appear / nothing happens:
<WindowProvider>wrapping your app?<Window>won’t mount without it.- Preload bundled? The import is side-effect-only — most bundlers need
sideEffectsin package.json or explicit inclusion. Check the renderer console for “preload bridge not found”. webPreferences.contextIsolation: true? ThecontextBridgeAPI requires it. The “preload bridge not found” warning lists this as a possible cause.manager.setupForWindow(mainWindow)called beforeloadFile/loadURL? ThesetWindowOpenHandlerneeds to be registered before the renderer runs.
“hit maxWindows (50)” with no obvious cause: Usually a <PooledWindow> with an unstable pool prop (inline createWindowPool(), useMemo with changing deps, or HMR without destroyWindowPool in import.meta.hot.dispose). See Pooling → Pool lifetime.
Styles missing in child window: injectStyles: "auto" copies <style>/<link> tags from the parent’s <head>. CSS-in-JS frameworks that inject styles elsewhere need injectStyles: false + their own injection logic.
Entry Points
Section titled “Entry Points”| Import | Use |
|---|---|
@loc/electron-window | Components and hooks (renderer) |
@loc/electron-window/main | setupWindowManager (main process) |
@loc/electron-window/preload | IPC bridge (preload script) |
@loc/electron-window/testing | Mocks for unit tests |
Next Steps
Section titled “Next Steps”- Props Reference — all
<Window>props - Hooks — reactive window state
- Pooling — pre-warm windows for instant display
- Persistence — save/restore bounds across sessions
- Testing — mock providers and event simulation
- Security — origin allowlist, ownership, enforced webPreferences
- Patterns — common recipes and integration notes