Window Pooling
For windows that appear and disappear frequently — overlays, HUDs, menus — pooling pre-warms hidden windows so display is instant.
Without pooling, each open={true} creates a new BrowserWindow and loads its content. With pooling, a window is ready before you need it.
import { PooledWindow, createWindowPool } from "@loc/electron-window";
// Create once at module level — NOT inside a componentconst overlayPool = createWindowPool( { transparent: true, frame: false }, // shape — creation-only props { minIdle: 1, maxIdle: 3, idleTimeout: 30000 }, // pool config);
function App() { const [show, setShow] = useState(false); return ( <PooledWindow pool={overlayPool} open={show} alwaysOnTop> <Overlay /> </PooledWindow> );}On open={true}: acquires a pre-warmed window from the pool instantly. On open={false}: hides and returns to pool — no destroy/recreate cost.
createWindowPool(shape, config?, options?)
Section titled “createWindowPool(shape, config?, options?)”Shape (creation-only props)
Section titled “Shape (creation-only props)”Shape props are fixed for the lifetime of the pool. All windows in the pool share the same shape.
| Prop | Description |
|---|---|
transparent | Transparent background |
frame | Window chrome |
titleBarStyle | Title bar appearance |
vibrancy | macOS vibrancy effect |
Changing these after pool creation has no effect — recreate the pool instead.
Pool config
Section titled “Pool config”| Option | Type | Default | Description |
|---|---|---|---|
minIdle | number | 1 | Minimum windows to keep warm in the pool |
maxIdle | number | 3 | Maximum idle windows to retain |
idleTimeout | number | 30000 | Milliseconds before excess idle windows are destroyed |
Pool options (third argument)
Section titled “Pool options (third argument)”The third argument to createWindowPool is an options object:
| Option | Type | Default | Description |
|---|---|---|---|
injectStyles | "auto" | false | (doc) => void | "auto" | How to copy styles into pool windows. false for CSS-in-JS frameworks. The function form replaces auto mirroring. |
onWindowSetup | WindowSetupCallback | — | Per-window setup hook. Runs once for each window the pool creates, after styles and #root are in place and before React portals content in. Must be synchronous. Additive — runs regardless of injectStyles. Optionally return a cleanup. |
const overlayPool = createWindowPool( { transparent: true, frame: false }, { minIdle: 1, maxIdle: 3 }, { injectStyles: false }, // CSS-in-JS: runtime handles injection);Per-window setup (options.onWindowSetup)
Section titled “Per-window setup (options.onWindowSetup)”Each pooled window is its own browsing-context realm — its own customElements registry, its own prototypes, its own document. Code that registers globals on the parent window at import time isn’t visible inside the pooled window.
onWindowSetup runs once per window the pool creates — including pre-warmed idle windows — right after <base>, styles, and the #root portal container are in place, and before React portals any content in. That ordering is the point: custom elements registered here upgrade synchronously as React inserts them, with no flash of unupgraded content the way a useLayoutEffect (which runs after mount) would risk. Use it for realm-specific initialization without giving up the default injectStyles: "auto" style mirroring:
const popoutPool = createWindowPool( { frame: true }, { minIdle: 1 }, { onWindowSetup: (childWindow, doc) => { // Register a custom element on the child realm if (!childWindow.customElements.get("my-widget")) { childWindow.customElements.define( "my-widget", class extends childWindow.HTMLElement {}, ); } // Patch a prototype, sync a documentElement attribute, etc. doc.documentElement.dataset.theme = document.documentElement.dataset.theme;
// Optional: cleanup runs when the window is destroyed (pool teardown, // idle eviction, or external close — not on hide/release back to pool). return () => { /* ... */ }; }, },);Compared to the function form of injectStyles, which replaces the auto <style>/<link> mirroring entirely, onWindowSetup is additive and runs in any injectStyles mode.
The hook must be synchronous. An async hook’s rejection escapes the error guard and its returned cleanup is silently dropped — TypeScript won’t warn (its void return rule is permissive), but the library detects it at runtime and logs an error. Do dynamic imports at module scope; keep the hook itself sync. The full contract — error containment, partial-failure rollback, the Window & typeof globalThis parameter type — is on the exported WindowSetupCallback type.
For non-pooled <Window>, you don’t need onWindowSetup — the window’s lifetime matches your component’s, so useWindowDocument() + useLayoutEffect does the same job at the same time.
<PooledWindow> Props
Section titled “<PooledWindow> Props”PooledWindow accepts most <Window> props except creation-only shape props (those are fixed by the pool). Behavior props — alwaysOnTop, title, event handlers, etc. — can vary per use and update live while open.
Not applicable to pooled windows (TypeScript error if passed):
- Shape props (
transparent,frame,titleBarStyle,vibrancy) — fixed by the pool definition injectStyles— fixed by the pool definition (third arg tocreateWindowPool)targetDisplay— pool windows are pre-created; this only affects placement at creation timepersistBounds— pool windows are reused; per-key persistence doesn’t fit the modelrecreateOnShapeChange— pool shape is immutable
| Prop | Type | Description |
|---|---|---|
pool | WindowPoolDefinition | Pool created by createWindowPool |
open | boolean | Acquire or release from pool |
All other <Window> props are supported.
Close button behavior
Section titled “Close button behavior”Pool windows use hideOnClose internally. When the user clicks the X button:
- The window hides (doesn’t destroy)
onUserClosefiresonClosedoes not fire (theBrowserWindowstill exists)
This differs from <Window>, where clicking X destroys the window and onClose fires. The typical pattern is the same though — your onUserClose handler sets open={false}, which releases the window back to the pool:
<PooledWindow pool={overlayPool} open={show} onUserClose={() => setShow(false)}>onClose fires only when the underlying BrowserWindow is actually destroyed — via destroyWindowPool(), main-process shutdown, or eviction when idle exceeds maxIdle.
Pool lifetime & cleanup
Section titled “Pool lifetime & cleanup”Pools outlive React. The pool instance is cached against the WindowPoolDefinition object’s identity and shared across every <PooledWindow> that receives it. Unmounting <PooledWindow> — or even <WindowProvider> — does not destroy the pool’s idle BrowserWindows. They stay hidden and warm, ready for the next mount.
This is usually what you want: the whole point of pooling is that the window already exists when you need it. But there are cases where you need to explicitly tear down a pool.
Keep the pool prop stable
Section titled “Keep the pool prop stable”The pool cache is keyed by object identity. If the WindowPoolDefinition you pass to <PooledWindow pool={...}> changes identity, the library creates a new pool — and the old pool’s idle windows are orphaned in the main process with nothing to clean them up.
// DON'T — new pool every render, leaks one hidden window per render<PooledWindow pool={createWindowPool({})} open={show}>
// DON'T — new pool whenever `userId` changes, leaks per user-switchconst pool = useMemo(() => createWindowPool({}), [userId]);
// DO — module-scope constant, stable for the life of the bundleconst overlayPool = createWindowPool({});function App() { return <PooledWindow pool={overlayPool} open={show}>...</PooledWindow>;}A dev-mode warning fires if <PooledWindow> sees a second distinct pool identity. The eventual production symptom is the maxWindows limit (50 by default) being hit with no obvious cause.
destroyWindowPool(poolDef)
Section titled “destroyWindowPool(poolDef)”Destroys all idle and active windows belonging to a pool and removes it from the cache. Call this when the pool will genuinely never be used again:
import { destroyWindowPool } from "@loc/electron-window";
// Route/feature teardown — you're done with the overlay feature entirelyuseEffect(() => { return () => destroyWindowPool(overlayPool);}, []);Calling this while a <PooledWindow> is mounted and open will close the user’s window out from under them. The next open={true} throws "Pool has been destroyed" (caught and dev-warned by <PooledWindow>). Only call it when no consumer is mounted.
HMR re-evaluating the module that defines your pool creates a fresh WindowPoolDefinition object — same identity-churn leak as inline creation, just slower. Dispose the old pool before the module reloads:
const overlayPool = createWindowPool( { transparent: true, frame: false }, { minIdle: 1 },);
if (import.meta.hot) { import.meta.hot.dispose(() => destroyWindowPool(overlayPool));}If you have several pools in one module, dispose each. The library has no way to do this automatically — it doesn’t know which module owns which pool.