Recipes
The API reference covers the primitives; these recipes cover the questions every adopter asks once the demo is on screen. Copy them, file off the parts that do not apply, ship.
Deep-link a feature
Section titled “Deep-link a feature”The playground reads ?demo=spotlight and opens the palette on load. Same pattern works for any system window or app.
import { useEffect } from "react";import { useWindowManager } from "@react-ui-os/core";import { SPOTLIGHT_OPEN_EVENT } from "@react-ui-os/desktop";
function DemoActivator() { const { openWindow } = useWindowManager(); useEffect(() => { const demo = new URLSearchParams(location.search).get("demo"); if (!demo) return;
switch (demo) { case "spotlight": window.dispatchEvent(new CustomEvent(SPOTLIGHT_OPEN_EVENT)); break; case "settings": openWindow({ kind: "system", systemId: "settings" }); break; case "notes": openWindow({ kind: "app", appId: "notes" }); break; } }, [openWindow]); return null;}
<Desktop apps={apps} theme={theme}> <DemoActivator /></Desktop>;The activator must be rendered as a child of <Desktop> so useWindowManager() can find the provider. See <Desktop children>.
Persist windows across reloads
Section titled “Persist windows across reloads”Pass a custom StorageAdapter that writes to your backend instead of localStorage. The window manager treats it the same way. The interface is synchronous: get returns a value now, not a promise. Back a remote store with an in-memory cache, hydrate it before mounting, and write through in the background.
import type { StorageAdapter } from "@react-ui-os/core";
function createRemoteStorage(initial: Record<string, unknown>): StorageAdapter { const cache = new Map<string, unknown>(Object.entries(initial)); const listeners = new Set<(key: string) => void>();
const events = new EventSource("/api/prefs/stream"); events.onmessage = (e) => { const { key, value } = JSON.parse(e.data) as { key: string; value: unknown }; cache.set(key, value); listeners.forEach((listener) => listener(key)); };
return { get<T = unknown>(key: string) { return cache.has(key) ? (cache.get(key) as T) : null; }, set(key, value) { cache.set(key, value); void fetch(`/api/prefs/${key}`, { method: "PUT", body: JSON.stringify(value) }); }, remove(key) { cache.delete(key); void fetch(`/api/prefs/${key}`, { method: "DELETE" }); }, subscribe(listener) { listeners.add(listener); return () => void listeners.delete(listener); }, };}
// Hydrate before mounting so the first synchronous read already has data.const initial = await fetch("/api/prefs").then((r) => r.json());
<Desktop apps={apps} theme={theme} storage={createRemoteStorage(initial)} />;A user’s accent, dock position, and window arrangement now follow them across devices.
Dynamic app registry
Section titled “Dynamic app registry”Apps don’t have to be a literal array; anything that resolves to an App[] works. Useful when a tenant’s plan determines which features show up.
import { useEffect, useState } from "react";import type { App } from "@react-ui-os/core";
function App() { const [apps, setApps] = useState<App[] | null>(null);
useEffect(() => { fetch("/api/me/apps") .then((r) => r.json()) .then((data: { id: string; name: string }[]) => { setApps( data.map((row) => ({ id: row.id, name: row.name, content: () => <RemoteAppFrame appId={row.id} />, })), ); }); }, []);
if (!apps) return <SplashScreen />; return <Desktop apps={apps} theme={theme} />;}The window manager re-keys windows when an app’s id changes, so swapping the registry on the fly works without manual cleanup.
Debounced Spotlight source
Section titled “Debounced Spotlight source”A source that calls a remote API on every keystroke is one network hop per character. Debounce inside the source closure.
import { useEffect, useRef } from "react";import { registerSpotlightSource } from "@react-ui-os/desktop";
export function RemoteSearchSource() { const cacheRef = useRef<Map<string, unknown[]>>(new Map()); const inflightRef = useRef<AbortController | null>(null);
useEffect(() => { return registerSpotlightSource("remote-search", (query) => { if (query.length < 2) return [];
// Synchronous return path: serve cached results immediately. const cached = cacheRef.current.get(query);
// Background fetch refreshes the cache; the next keystroke (or // an external listSpotlightSources notification) shows new data. inflightRef.current?.abort(); const ac = new AbortController(); inflightRef.current = ac; void fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: ac.signal }) .then((r) => r.json()) .then((rows) => { cacheRef.current.set(query, rows); }) .catch(() => { // Aborted by next keystroke, fine. });
return (cached ?? []).map((row: any) => ({ id: `remote:${row.id}`, name: row.title, tagline: row.url, kindLabel: "Search", onActivate: () => window.open(row.url, "_blank", "noopener"), })); }); }, []);
return null;}The first keystroke shows nothing because the cache is cold, but a few characters in the cache catches up and results appear without flicker.
Open a window from a URL route
Section titled “Open a window from a URL route”The window manager doesn’t own routing, but it doesn’t fight it either. Route components mount, dispatch openWindow, then return null: the route is just a hook.
// app/(routes)/files/page.tsximport { useEffect } from "react";import { useWindowManager } from "@react-ui-os/core";
export default function FilesRoute() { const { openWindow } = useWindowManager(); useEffect(() => { openWindow({ kind: "system", systemId: "recents" }); }, [openWindow]); return null;}openWindow collapses repeat calls with the same payload, so refreshing the route does not open a second window.
Per-instance system window arguments
Section titled “Per-instance system window arguments”A system window can be opened with args and the name resolved per instance. Two windows with different args coexist as distinct windows.
import { registerSystemWindow } from "@react-ui-os/desktop";
registerSystemWindow("inspector", { name: ({ targetId }) => `Inspector: ${String(targetId)}`, defaultBounds: { w: 360, h: 480 }, content: ({ args }) => <InspectorContent id={String(args?.targetId)} />,});
// Two open at once, one per target:openWindow({ kind: "system", systemId: "inspector", args: { targetId: "a-1" } });openWindow({ kind: "system", systemId: "inspector", args: { targetId: "a-2" } });See SystemWindowArgs for the underlying serialization rules.
Conditional desktop folders
Section titled “Conditional desktop folders”A folder icon should only show up when there’s something in it. appearsAsDesktopIcon is the gate: it reads from the storage adapter so the same predicate sees per-user state in a multi-tenant deploy.
import { registerSystemWindow } from "@react-ui-os/desktop";import { hasRecents } from "./recents";
registerSystemWindow("recents", { name: "Recents", defaultBounds: { w: 560, h: 420 }, content: RecentsFolder, appearsAsDesktopIcon: (storage) => hasRecents(storage),});hasRecents reads storage.get("recents"); when the user empties Recents and the storage write fires its change event, the icon disappears in the same tick.
See also
Section titled “See also”useWindowManagerfor the imperative API every recipe leans on.registerSpotlightSourcefor source mechanics.- Use cases for whole product shapes that combine these recipes.
- Showcase for end-to-end implementations.