---
title: useDeskState
description: Which windows are open, where they sit and what has been put away, remembered without breaking a server render.
source: https://adamjurek.com/components/use-desk-state
---

## Home


<!-- ::start:showcase demo="use-desk-state" height="300" -->
<!-- ::end:showcase -->

## Paths are stored as ids

A stored desk outlives the tree it described. Components get added and renamed,
packages get removed.

Storing the entries themselves would restore a window titled after something
that is no longer there; storing ids means a path that no longer resolves is
dropped, and the window quietly does not reopen. Quiet is right - somebody
returning to the site did not ask about your refactor.

## Where this is used

`FolderShelf` holds one internally when given `rememberAs`. The home page holds
its own instead, because two things need it: the shelf draws the windows and the
dock lists them, and the place where two components meet is the component above
both.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/use-desk-state.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/use-desk-state.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Overlays |
| Files | `use-desk-state.ts` |
| Dependencies | None |
| Tags | state, storage, ssr, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { useDeskState } from "@sushindustries/ui";

export function Desktop() {
	const desk = useDeskState("my.desk");

	return (
		<button type="button" onClick={() => desk.open(["components", "motion"])}>
			Open Motion
		</button>
	);
}
```

## What you should see

Nothing on first render - this is a hook, not a component. Click the button
and `desk.windows` gains an entry for that path; render your own window
around each one. Reload the page and the same window reopens where it was
left, because the desk was written to `localStorage` under the key you gave
it.

## If nothing happens

`desk.ready` is `false` on the very first render, on both the server and the
client, even when storage has a saved desk. That is deliberate - rendered
output must not depend on `ready` for what it shows, only for whether it
animates a restored window into place. If a window seems to vanish and
reappear a moment later, something downstream is branching on `ready`
instead of just rendering `desk.windows`.


## Guides


## 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.

```ts
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]);
```

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.

## `toggle` is the taskbar's press

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

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

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

```ts
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
```

`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.


## API


<!-- generated:api -->

## Signature

```ts
useDeskState(key: string): DeskApi
```

The desk: which windows are open, where they are, and what has been put away. Two rules keep this safe to render on a server. **The first render is always the empty desk**, on the server and on the client alike, and storage is read in an effect afterwards. Reading localStorage during render produces markup the server could not have sent, and React answers a mismatch by discarding the tree and rebuilding it - which on a page of icons means every icon briefly has no working click handler. That failure has already happened here once. **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 say to somebody about it. Paths are stored as ids rather than as entries, because the stored desk outlives the tree it described: components get added and renamed, and a window pointing at a folder that no longer exists should quietly not reopen rather than restore a window onto nothing.

<!-- /generated:api -->

## Notes

`key` is the `localStorage` key, and it is also the identity boundary: two
`useDeskState` calls with the same key share one stored desk, which is how
`FolderShelf` and a dock built above it can agree about what is open without
being the same component. Two calls with different keys are two unrelated
desks that happen to render on the same page.

`raise` returns the same state object rather than a new one when the target
is already front-most, so calling it on every click of an already-focused
window does not re-render anything and does not push the `z` counter any
higher. Everything else always produces a new object, even when the change
is a no-op, because there was nothing cheap to check.



## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="use-desk-state" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

The home page's own shelf holds one desk and hands `open` to every icon,
whatever kind of thing it opens:

```tsx
import { useDeskState } from "@sushindustries/ui";

const DESK_KEY = "sushindustries.desk";

export function SiteShelf({ entries }: { entries: ShelfEntry[] }) {
	const desk = useDeskState(DESK_KEY);

	function open(entry: ShelfEntry, path: ShelfEntry[] = []) {
		desk.open([...path.map((step) => step.id), entry.id]);
	}

	return (
		<>
			{entries.map((entry) => (
				<button key={entry.id} type="button" onClick={() => open(entry)}>
					{entry.title}
				</button>
			))}
			{desk.desk.windows.map((win) => (
				<DeskWindow key={win.id} state={win} onClose={() => desk.close(win.id)} />
			))}
		</>
	);
}
```

## What this example is not

This does not render the windows themselves - `DeskWindow` is left as a
stand-in for whatever component draws a window from a `DeskWindowState`.
`useDeskState` only tracks which windows exist and where; drawing them,
including drag-to-move calling `desk.move`, is a separate concern.
