Skip to content

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 component
const 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 props are fixed for the lifetime of the pool. All windows in the pool share the same shape.

PropDescription
transparentTransparent background
frameWindow chrome
titleBarStyleTitle bar appearance
vibrancymacOS vibrancy effect

Changing these after pool creation has no effect — recreate the pool instead.

OptionTypeDefaultDescription
minIdlenumber1Minimum windows to keep warm in the pool
maxIdlenumber3Maximum idle windows to retain
idleTimeoutnumber30000Milliseconds before excess idle windows are destroyed

The third argument to createWindowPool is an options object:

OptionTypeDefaultDescription
injectStyles"auto" | false | (doc) => void"auto"How to copy styles into pool windows. false for CSS-in-JS frameworks.
const overlayPool = createWindowPool(
{ transparent: true, frame: false },
{ minIdle: 1, maxIdle: 3 },
{ injectStyles: false }, // CSS-in-JS: runtime handles injection
);

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 to createWindowPool)
  • targetDisplay — pool windows are pre-created; this only affects placement at creation time
  • persistBounds — pool windows are reused; per-key persistence doesn’t fit the model
  • recreateOnShapeChange — pool shape is immutable
PropTypeDescription
poolWindowPoolDefinitionPool created by createWindowPool
openbooleanAcquire or release from pool

All other <Window> props are supported.

Pool windows use hideOnClose internally. When the user clicks the X button:

  1. The window hides (doesn’t destroy)
  2. onUserClose fires
  3. onClose does not fire (the BrowserWindow still 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.

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.

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-switch
const pool = useMemo(() => createWindowPool({}), [userId]);
// DO — module-scope constant, stable for the life of the bundle
const 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.

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 entirely
useEffect(() => {
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.