useDeskState

@sushindustries/ui

Which windows are open, where they sit and what has been put away, remembered without breaking a server render.

3 min read
View as Markdown

Two rules make it safe to render on a server

The first render is always the empty desk, on the server and on the client alike. Storage is read in an effect afterwards.

const [desk, setDesk] = useState<DeskState>(EMPTY_DESK);

useEffect(() => {
	try {
		const stored = window.localStorage.getItem(key);
		if (stored) setDesk({ ...EMPTY_DESK, ...JSON.parse(stored) });
	} catch {
		// A default desk is a working desk.
	}
	setReady(true);
}, [key]);
ts

Reading localStorage during render produces markup the server could not have sent. React answers a mismatch by discarding the tree and rebuilding it, and on a page of icons the cost of that is every icon briefly having no working click handler. That has already happened here once, from a typeof document branch.

None of it is required. Where a window sits is a preference about a decoration. If storage is full, disabled, or in a browsing mode that refuses it, the desk is simply the one everybody else gets - so every access is wrapped and every failure is silent. There is nothing useful to tell somebody about it.

One window per folder

Opening a folder that is already open raises it rather than stacking a second identical window on the first. That is what people expect, and it is also what stops an impatient double-click producing two windows.

toggleToggleA button that stays down, and the single-select group of them - aria-pressed is the whole contract.@sushindustries/ui · content is the taskbar's press

Minimised, it comes back and comes forward. Behind, it comes forward. Already in front, it goes away.

<Dock tasks={tasks} onSelectTask={desk.toggle} onCloseTask={desk.close} />
tsx

That last case is what makes a taskbar a taskbar rather than a list of links, and it lives here rather than in the dock because it is a decision about state. The dock presses; the desk decides.

A minimised window keeps its position, its size and its place in the stack. It is a flag rather than a second list, because it is the same window with somewhere else to be.

raise does nothing when it can

Raising the front-most window returns the same state object, so React does not re-render and the z counter does not climb forever on repeated clicks. A counter that only goes up is fine until it is serialised into storage on every press.

The API

const desk = useDeskState("my.desk");

desk.open(["components", "motion"]);
desk.navigate(id, ["components"]);
desk.move(id, x, y);
desk.resize(id, w, h);
desk.raise(id);
desk.toggle(id);         // what a taskbar press does
desk.close(id);

desk.hide(entryId);      // take an icon off the desktop
desk.restore(entryId);
desk.reset();            // put everything back
ts

ready turns true once storage has been read. Rendered output must not depend on it - it exists so a consumer can avoid animating a restored window into place.

Used byFolder ShelfFolder ShelfA desktop of folders that open into draggable windows, several at once, remembered between visits.@sushindustries/ui · layout