StorageAdapter
The library owns persistence but lets the consumer swap the backend. The default adapter is localStorage-backed; pass your own to <Desktop> for server-side sync, cross-device storage, encrypted local stores, or to mock during tests.
Import
Section titled “Import”import { createLocalStorageAdapter, type StorageAdapter } from "@react-ui-os/core";Interface
Section titled “Interface”| Field | Type | Description |
|---|---|---|
get | <T = unknown>(key: string) => T | null | · |
set | <T>(key: string, value: T) => void | · |
remove | (key: string) => void | · |
subscribe | (listener: (key: string) => void) => () => void | Notify a listener whenever a stored value changes. Returns an unsubscribe function. Listeners receive the unprefixed key. |
The library passes unprefixed keys ("settings:default", "recents", etc.). It’s the adapter’s job to namespace them however it wants.
The default adapter
Section titled “The default adapter”const storage = createLocalStorageAdapter(); // prefix "rui-os"// orconst storage = createLocalStorageAdapter("acme-os"); // namespaceBehavior:
- Writes JSON-stringify the value and call
localStorage.setItem(prefix:key, json). - Dispatches a
CustomEvent("react-ui-os:storage-changed", { detail: { key } })for in-tab subscribers. - Listens to the native
storageevent for cross-tab updates. - SSR-safe: every method short-circuits when
windowis undefined.
Custom backends
Section titled “Custom backends”Server-backed
Section titled “Server-backed”const remote: StorageAdapter = { get: (key) => fetchPref(key), set: (key, value) => savePref(key, value), remove: (key) => deletePref(key), subscribe: (listener) => { const source = new EventSource("/prefs/stream"); source.onmessage = (e) => listener(JSON.parse(e.data).key); return () => source.close(); },};
<Desktop apps={apps} theme={theme} storage={remote} />;Encrypted localStorage
Section titled “Encrypted localStorage”Wrap the default adapter and run encrypt / decrypt on the values:
const base = createLocalStorageAdapter();const encrypted: StorageAdapter = { ...base, get: (key) => { const raw = base.get<string>(key); return raw ? JSON.parse(decrypt(raw)) : null; }, set: (key, value) => base.set(key, encrypt(JSON.stringify(value))),};In-memory mock for tests
Section titled “In-memory mock for tests”function createMemoryAdapter(): StorageAdapter { const store = new Map<string, unknown>(); const listeners = new Set<(key: string) => void>(); return { get: (key) => (store.get(key) ?? null) as never, set: (key, value) => { store.set(key, value); listeners.forEach((l) => l(key)); }, remove: (key) => { store.delete(key); listeners.forEach((l) => l(key)); }, subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); }, };}Keys the library writes
Section titled “Keys the library writes”| Key | Written by |
|---|---|
settings:<themeId> | useSettings().setPref and Settings UI |
| Anything you write | Consumer code (the docs Recents demo writes "recents") |
Reads via get<T>(...) are typed by the caller; the adapter doesn’t enforce.
Subscribe semantics
Section titled “Subscribe semantics”subscribe(listener) calls listener(key) whenever any value changes. Cross-tab storage events translate to the same callback. The returned function unsubscribes.
The library uses one subscription per <DesktopProvider> instance to invalidate user prefs and to re-evaluate appearsAsDesktopIcon predicates.
See also
Section titled “See also”useSettings: the most common writer.- DesktopIcons: state-earned folder predicates read from this adapter.