Summary
There is no reactive form primitive in stx. useForm does not exist anywhere in packages/stx/src. defineForm exists (packages/stx/src/forms-validation.ts:549) but is not exported from the package entry and is not a runtime global, so no application can reach it; and its values/errors are plain objects rather than signals, so even reached it would not drive a template. useMutation is the only shipped mutation primitive and it throws on !response.ok before reading the body, which makes the server's {message} — the one thing a real form has to display — unreachable.
The consequence in practice is that every form in an app is hand-rolled, identically, once per page.
Impact (real app)
Six near-identical submit handlers in analyticshq: resources/views/login.stx:25-62, register.stx:17-84, sites/new.stx:31-71, pricing.stx:28-52, reset-password.stx:48-96, forgot-password.stx:16-69.
Each one opens with the same two declarations (login.stx:14-15, register.stx:14-15, sites/new.stx:18-19, pricing.stx:23-24, reset-password.stx:16-18, forgot-password.stx:12-14):
const error: StxSignal<string> = state('')
const loading: StxSignal<boolean> = state(false)and then repeats, byte for byte: e.preventDefault(), the re-entrancy guard if (loading()) return, loading.set(true); error.set(''), headers: {'Content-Type':'application/json'}, JSON.stringify(...), await res.json().catch(() => ({})), if (!res.ok) error.set(data.message || '<fallback>'), catch { … }, finally { loading.set(false) }.
The catch block is literally identical in all six files:
resources/views/login.stx:57 error.set('Something went wrong. Please try again.')
resources/views/register.stx:79 error.set('Something went wrong. Please try again.')
resources/views/pricing.stx:47 err.set('Something went wrong. Please try again.')
resources/views/reset-password.stx:91 error.set('Something went wrong. Please try again.')
resources/views/forgot-password.stx:64 error.set('Something went wrong. Please try again.')
resources/views/sites/new.stx:68 error.set('Something went wrong. Please try again.')Because the only error channel is one error signal, no form in the app has field-level errors. A multi-field validation failure can render exactly one message.
Evidence in stx
defineForm is defined but unreachable:
packages/stx/src/forms-validation.ts:549—export function defineForm<T>(schema, initialValues?): FormState<T>packages/stx/src/index.ts:54—export * from './forms'; there is noexport * from './forms-validation'anywhere inindex.ts, andforms.tsdoes not re-export it. The only two files that reference the module areprocess.ts:1395andlazy-module.ts:7, both internal.grep -c defineForm packages/stx/dist/index.js→0.dist/index.d.ts→ no match.packages/stx/src/runtime-globals.ts:43-58—STX_RUNTIME_GLOBALSdoes not listdefineForm, so it is not destructured into any client script either.
Even if it were exported, it is not reactive — packages/stx/src/forms-validation.ts:556-568:
const values = {} as { [K in keyof T]: unknown }
const errors = {} as { [K in keyof T]: string[] }
const touched = {} as { [K in keyof T]: boolean }Plain objects; mutating errors[field] at :576 notifies nothing.
useMutation cannot serve as the fallback — packages/stx/src/signals.ts:1324-1326:
var response = await __stxFetch('useMutation', resolvedUrl, fetchOpts);
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
var result = await response.json();The body is never read on failure, so data.message is gone.
Note for triage: #1843's table lists defineForm and defaultFormClasses under "already-exported stx API". For defineForm on 0.2.155 that is not the case — the export is missing from the entry point.
Prior art
- Inertia:
useForm({ email: '', password: '' })→form.processing,form.errors.email,form.post('/login'),form.reset(). - React Hook Form + TanStack
useMutation(isPending,error,mutate). - Angular reactive forms:
FormGroup+control.errors. - Laravel Precognition for live server-side validation into the same error bag.
Ask
- Export
defineForm(andv) from the package entry and add both toSTX_RUNTIME_GLOBALSso they resolve inside<script client>. - Back
values/errors/touched/dirtywith signals soform.errors.emaildrives a template binding. - Ship
useForm(initialValues, { schema? })returning{ values, errors, processing, submit(url, opts), reset() }, wired to the data layer sosubmit()owns the re-entrancy guard,Content-Type,JSON.stringify, and error mapping. - Make
useMutationread the response body on failure and expose it (error().data,error().status), so{message}/{error}/ a field-error bag can be mapped intoform.errorswithout an app rewriting the fetch.