ReviewOS

stacks/stx

useLocalStorage has five different return shapes across stx and the Stacks packages

#1797
Closed glennmichael123 opened this 10 days ago ·
10 days ago

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.

WhereReturnsRead a value
stx.d.ts ambient (what a <script client> block sees)StxSignal<T>s()
signals runtime (window.stx.useLocalStorage)signals()
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:

  1. useLocalStorage is not exported from the package index at all. import { useLocalStorage } from '@stacksjs/stx' fails with TS2614: Module '"@stacksjs/stx"' has no exported member 'useLocalStorage', while state/derived import fine. A functions/ composable — the layout stx's own docs recommend for anything with persistence — therefore cannot use it without reaching for the /composables subpath, which returns a different shape than the same identifier does inside a <script client> block.

  2. 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 the browser-composables shape; 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 / useSessionStorage return a Signal on every path, matching the ambient type and the runtime.
  • useStorage() keeps returning StorageRef — it is the richer API (options, custom serializer, remove(), subscribe()) and is genuinely a different thing.
  • Export both from the package index so functions/*.ts can import them.
  • Retire or clearly deprecate browser-composables.ts.
  • Add a parity test alongside the existing use-cookie-parity / use-fetch-parity / use-optimistic-parity suites.

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.

Sign in to comment on this issue.