# usePersistedState

> A React useState that survives reloads in localStorage, sessionStorage or a cookie, syncs across tabs and hydrates cleanly.

- Type: React hook (Hooks & Utilities)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add use-persisted-state`
- Docs: https://wingo-ui.com/components/use-persisted-state

## Interaction

Switch the listing view in either card: the other card follows at once because both read the same key. Type a message: it is saved as you type. Press Reload to remount the demo and watch every value come back, or Delete draft to forget the draft. Open the page in a second tab and changes in local storage follow you there.

## Installation

```bash
npx wingo-ui@latest add use-persisted-state
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/use-persisted-state` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `hooks/use-persisted-state.ts`

Wingo UI dependencies (installed automatically):

- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))

## Usage

```tsx
import { usePersistedState } from "@/hooks/use-persisted-state"

export function ListingsView() {
  const [view, setView] = usePersistedState<"list" | "grid">("listings:view", "list")

  return (
    <div role="group" aria-label="View" className="inline-flex gap-1">
      <button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button>
      <button type="button" aria-pressed={view === "grid"} onClick={() => setView("grid")}>Grid</button>
    </div>
  )
}
```

## API

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string \| null \| undefined` | `"ui-lab:preferences"` | usePersistedState(key, defaultValue, options?): the storage key. null or an empty string turns persistence off (plain useState), so a component can take an optional persistKey prop. |
| `defaultValue` | `T` |  | The value on the server, during hydration and whenever nothing is stored. The stored value replaces it right after hydration, so markup never mismatches. |
| `storage?` | `"local" \| "session" \| "cookie"` | `"local"` | Where the value lives. cookie is the one a server can read: a Next layout reads cookies().get(persistedCookieName(key)) and passes the value as defaultValue, so a collapsed sidebar is collapsed on the first paint. |
| `syncTabs?` | `boolean` | `true` | Follow changes made in other tabs (local storage only). Instances on the same page always stay in sync. |
| `serialize / deserialize?` | `(value: T) => string / (raw: string) => T` |  | How the value becomes a string and back. Default JSON; a stored value that fails to parse falls back to defaultValue. |
| `maxAge?` | `number` | `31536000` | Cookie lifetime in seconds (one year). |
| `returns?` | `[value: T, setValue: (next: SetStateAction<T>) => void, meta: { hydrated: boolean; remove(): void }]` |  | Like useState. meta.hydrated is false on the server and during hydration, so a component can skip the animation of the restore that follows. meta.remove forgets the stored value. Storage that throws (private mode, a full quota, blocked cookies) keeps working in memory for the session. |

## Parts

### usePersistedControllableState (hook)

The controlled / uncontrolled pattern of useControllableState with the uncontrolled value persisted: collapsed / defaultCollapsed / onCollapsedChange plus persistKey in one line. A controlled value is never written to storage. Signature: ({ key, value?, defaultValue, onChange?, ...options }) => [T, setValue, meta].

### readPersisted (function)

Reads the raw stored string outside React (event handlers, other stores). Signature: (key, storage?) => string | null.

### writePersisted (function)

Writes or removes (null) the raw string outside React; every hook reading the key updates. Signature: (key, raw | null, storage?, maxAge?) => void.

### persistedCookieName (function)

The cookie name a key is stored under, for server code: cookies().get(persistedCookieName(key)). Signature: (key) => string.

### PersistedStorage (type)

"local" | "session" | "cookie", where the value lives.

### UsePersistedStateOptions (type)

The options object: storage, serialize, deserialize, maxAge and syncTabs.

### PersistedStateMeta (type)

The third tuple item: { hydrated: boolean; remove(): void }.

### UsePersistedControllableStateParams (type)

The options of usePersistedControllableState: key, value, defaultValue, onChange plus the storage options.

## Examples

- **Remembered settings**: Two switches kept in local storage. Flip them, press Reload, and they come back as you left them.
- **Draft for this tab only**: storage="session" keeps an unsent message until the tab closes.
- **Cookie the server can read**: storage="cookie" so a layout can render the saved view on the first paint.

## See also

- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useDebounce](https://wingo-ui.com/components/use-debounce) ([Markdown](https://wingo-ui.com/components/use-debounce.md))
- [useConsent](https://wingo-ui.com/components/use-consent) ([Markdown](https://wingo-ui.com/components/use-consent.md))
- [Theme Toggle](https://wingo-ui.com/components/theme-toggle) ([Markdown](https://wingo-ui.com/components/theme-toggle.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))

---

Canonical: https://wingo-ui.com/components/use-persisted-state
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
