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 |
Style injection (options.injectStyles)
Section titled “Style injection (options.injectStyles)”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. |
const overlayPool = createWindowPool( { transparent: true, frame: false }, { minIdle: 1, maxIdle: 3 }, { injectStyles: false }, // CSS-in-JS: runtime handles injection);<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.