Summary
composables/ exports 36 composables. 29 of them never reach the client runtime, and 22 of those wrap APIs that exist only in a browser — clipboard, geolocation, battery, fullscreen, notifications, speech, wake lock, the observers, and more.
They are exported from composables/index.ts, they type-check, they autocomplete, and they are unreachable from the only environment where their underlying API exists.
Calling one from a <script client> block throws ReferenceError and kills the entire client block, so the page stops hydrating and renders raw to the user.
Reproduction
<script client>
const kind = state('start')
const c = useClipboard()
kind.set('resolved:' + typeof c)
</script>
<main><p id="probe">{{ kind() }}</p></main>Rendered output:
rendered :
error : ReferenceError: useClipboard is not definedThe literal in the DOM is the important part: one unresolvable name does not degrade that one call, it takes down every binding in the block.
The 22
Diffing composables/*.ts against the use* keys actually present on window.stx in a live page:
composables/ files : 36
on window.stx : 35 (a mostly disjoint set — see below)Browser-only, exported, unreachable:
useBattery, useBroadcastChannel, useClipboard, useDeviceOrientation, useEventSource, useEyeDropper, useFullscreen, useGeolocation, useIdle, useIntersectionObserver, useKeyboard, useMouse, useMutationObserver, useNetwork, useNotification, usePermissions, useResizeObserver, useShare, useSpeech, useTextSelection, useWakeLock, useWindow
Also missing but not browser-gated: useChannel, useMeta, useRouter, useStorage, useTimer, useUtilities, useWebsocket.
Root cause
There are two parallel, hand-maintained implementations of the composable surface with no shared source:
src/composables/*.ts— a module tree, importable server-side, 36 entriessrc/signals.ts:5458— the client runtime'swindow.stx = { … }literal, with its own inline reimplementations, 35use*entries
A composable added to the first does not appear in the second. useClipboard has existed in composables/use-clipboard.ts with full JSDoc and a ClipboardRef interface the whole time; it has simply never been reachable from a browser.
The name is also in neither STX_AUTO_IMPORTS (client-script.ts:308) nor the runtime, which is why it degrades to a bare ReferenceError rather than the undefined is not a function that #1804 describes. Those are the two halves of the same drift.
A second defect in the canonical implementation
Worth fixing at the same time, because it will bite whoever wires this up first. composables/use-clipboard.ts exposes copied as a getter plus a subscribe(), and auto-resets the flag on a timer:
copiedTimeout = setTimeout(() => { copied = false }, timeout)There is no notify() in that callback. subscribe() fires on copy and never on the reset. So the intended "Copied!" → "Copy" affordance cannot be built from the public surface: a subscriber mirroring copied into a signal latches at true forever. The timer's only purpose is to flip a flag nobody can observe flipping.
(onDestroy is also absent here, so copiedTimeout outlives the component. The deprecated browser-composables.ts copy does clear it.)
Suggested fix
- Generate the client runtime's composable surface from
composables/, or add a test asserting the two sets agree. One hand-maintained list mirroring another is the actual bug;useClipboardis a symptom. - Until then, at minimum port the 22 browser-only ones — they are the set for which "server-side only" is a contradiction.
- Make an unresolvable auto-import name a build-time error. Silently emitting a call to an undefined binding turns a typo into a blank page.
- Call
notify()in the reset timeout, and registeronDestroyto clear it.
Point 3 stands on its own regardless of the rest: today, one bad name in a client block costs the whole page, at runtime, in production.
Found while replacing hand-rolled navigator.clipboard calls in a production stx app with the framework's own composable. Related: #1804, #1797.