Surfaced while fixing #1796. That issue was a declaration-emit bug; this is the reason it survived unnoticed — there is no single expected shape to check a declaration against.
| Where | Returns | Read a value |
|---|---|---|
stx.d.ts ambient (what a <script client> block sees) | StxSignal<T> | s() |
signals runtime (window.stx.useLocalStorage) | signal | s() |
composables/use-storage.ts (@stacksjs/stx/composables) | StorageRef<T> | s.value / s.get() |
browser-composables.ts (shipped in dist) | { value, remove } | s.value |
packages/components/src/composables/utilities/useLocalStorage.ts | { value, setValue, removeValue } | s.value |
@stacksjs/composables (separate package) | Ref<T> | s.value |
The ambient type and the runtime agree, which is the path most app code takes, so this mostly bites when someone imports rather than using the bare global.
Two concrete consequences:
useLocalStorageis not exported from the package index at all.import { useLocalStorage } from '@stacksjs/stx'fails withTS2614: Module '"@stacksjs/stx"' has no exported member 'useLocalStorage', whilestate/derivedimport fine. Afunctions/composable — the layout stx's own docs recommend for anything with persistence — therefore cannot use it without reaching for the/composablessubpath, which returns a different shape than the same identifier does inside a<script client>block.Downstream docs describe shapes that don't exist on the path they describe. At least one Stacks app's agent skill documents
const { value, remove } = useLocalStorage(key, default)for stx templates. That is thebrowser-composablesshape; a<script client>block gets a signal, so the destructure yields undefined.
browser-composables.ts also looks vestigial: it is not re-exported from src/index.ts or composables/index.ts, and nothing in the repo imports it, yet it ships as dist/browser-composables.d.ts and is reachable via the ./* wildcard export.
Suggested direction
#1710 already set the precedent for exactly this situation — it converted useCookie from a Vue-style CookieRef to a Signal so both implementations returned the same shape, and pinned it with test/signals/use-cookie-parity.test.ts. The same move here:
useLocalStorage/useSessionStoragereturn a Signal on every path, matching the ambient type and the runtime.useStorage()keeps returningStorageRef— it is the richer API (options, custom serializer,remove(),subscribe()) and is genuinely a different thing.- Export both from the package index so
functions/*.tscan import them. - Retire or clearly deprecate
browser-composables.ts. - Add a parity test alongside the existing
use-cookie-parity/use-fetch-parity/use-optimistic-paritysuites.
Worth confirming the breaking-change appetite for the /composables subpath before doing it — that is the one path where the shape would actually change for an existing consumer.