Repository navigation
Remote form fields.value() diverges from submitted FormData — proposal for dev-mode consistency check #16015
Description
Describe the problem
When using remote forms, there are two representations of "the form's data" that can silently diverge:
- Internal state — what
fields.value()returns (starts as{}) - Submitted data — what the server actually receives (serialized from DOM elements into FormData)
These two should always agree. In practice, they frequently don't — and the current API provides no mechanism to detect or prevent this divergence.
Direction 1: DOM has a value, internal state doesn't
<form {...form}>
<!-- DOM shows "200", server receives "200", but fields.price.value() is undefined -->
<input {...form.fields.price.as('number', 200)} />
<!-- Client-side logic breaks because value() doesn't reflect what's visible -->
{#if form.fields.price.value() !== undefined}
<p>Price: {form.fields.price.value()}</p> <!-- Never renders! -->
{/if}
</form>.as('number', 200) sets the DOM's defaultValue to 200, but the internal state (let input = $state({})) is never updated. The user sees "200", the server receives "200", but any client-side logic using value() sees undefined.
This is the same root cause as #15937 — where clearing a number field is impossible because the form re-asserts the defaultValue.
Direction 2: Internal state has a value, DOM doesn't
<script>
// Developer correctly initializes the field...
form.fields.itemId.set("existing-id");
// ...but forgets to render a corresponding input
</script>
<form {...form}>
<input {...form.fields.name.as('text')} />
<!-- No <input {...form.fields.itemId.as("hidden")} /> — silent data loss! -->
<button>Save</button>
</form>The server never receives itemId. No error, no warning. The developer thinks they're submitting it (they even called .set() correctly), but it disappears because there's no DOM element to serialize into FormData.
This is exactly the scenario reported in #14990 — "if you forget to add {...form.fields.field.as(...)} for a required field, nothing will happen at all. No errors will get thrown, no warnings."
Real-world consequences
In our production app (~30 remote forms), we've accumulated these workarounds:
// 1. Schema defaults don't propagate to client state — must imperatively .set()
form.fields.versions.set([]);
form.fields.majorVersions.set([]);
// 2. RemoteFormInput doesn't accept `undefined` in its index signature.
// Schema with .default([]) produces input type `T[] | undefined`, which fails form().
// Workaround: cast away the default's effect on the input type (loses type safety)
majorVersions: majorVersionDraftsSchema.default([]) as unknown as typeof majorVersionDraftsSchema
// 3. Array iteration requires length check + filter (proxies aren't iterable),
// AND schema.parse() to narrow DeepPartial<T> back to T
new Array(form.fields.versions.value().length)
.fill(null, 0)
.map((_, i) => form.fields.versions[i])
.filter(v => v !== undefined)
.map(field => ({ field, value: versionDraftSchema.parse(field.value()) }))
// 4. Edit forms need .set() at top-level for SSR AND inside $effect.pre for reactivity
// svelte-ignore state_referenced_locally
form.fields.set(app);
$effect.pre(() => { form.fields.set(app); });Each workaround exists because the form's internal state and its DOM representation are independently managed with no synchronization.
The DeepPartial type problem (workaround #3)
Because internal state starts as {}, value() must return DeepPartial<T> — every property is recursively optional:
// The schema type:
type Version = { number: number; changeLog: string; majorVersion: number }
// What value() actually returns:
type FromValue = { number?: number; changeLog?: string; majorVersion?: number } | undefinedThis makes value() results unusable anywhere the full schema type is expected. The workaround is calling schema.parse(field.value()) — which validates AND narrows the type back to T. This adds runtime overhead and boilerplate for what is fundamentally a type-level consequence of the empty-initialization design.
If the form guaranteed complete initialization (from schema defaults or mandatory initial values), value() could return T directly — eliminating both the type unsafety and the need for runtime re-parsing.
The architectural issue
The current design stores form state in let input = $state({}) which starts empty. When .as(type, defaultValue) is called, it sets the DOM defaultValue attribute but does not update input. The value() method reads exclusively from input. This creates a persistent split:
value()returnsundefinedfor every field until explicitly.set()- The DOM shows whatever
defaultValuewas passed to.as() - The server receives whatever the DOM serializes
- The TypeScript return type must be
DeepPartial<T>to reflect this uncertainty
Proposed: dev-mode consistency check
During development, when a form is submitted, compare the internal state (fields.value()) with the FormData being sent. If they diverge, emit a console warning:
Direction 1 (DOM → state):
⚠️ [svelte-kit] Form field "price" has value `undefined` in fields.value()
but will be submitted as "200" (from DOM defaultValue).
This can cause issues if you rely on fields.value() for client-side logic.
Consider calling fields.price.set(200) to synchronize.
Direction 2 (state → DOM):
⚠️ [svelte-kit] Form field "itemId" has value "existing-id" in fields.value()
but no corresponding <input> was found in the submitted FormData.
Did you forget to add <input {...form.fields.itemId.as("hidden")} />?
Summary:
fields.value() |
FormData (submitted) | Result |
|---|---|---|
undefined |
"200" sent |
.set() — client logic sees stale state |
"existing-id" |
field missing | <input> — server never receives it |
"hello" |
"hello" |
✅ Consistent |
This is low-cost to implement (a comparison in handle_submit before sending), non-breaking, and would catch an entire class of bugs at development time. It also buys time to figure out the larger architectural question of how initialization should work.
Related open issues
- Using a remote number form field with a default value stops you from completely deleting the field value #15937 — Number field
.as('number', 200)prevents clearing (confirmed bug) - fields.value() returning undefined when input_value is passed to .as() #15707 / PR fix: make form fields.value() reflect input_value passed to .as() #15690 —
fields.value()returns undefined wheninput_valuepassed to.as()(PR stalled — maintainer notes "points at larger issue with initial state") - Remote Forms not handling null values #15249 / PRs fix: add
nullandundefinedtoRemoteFormInputtype #15859, fix:AsArgsdefault value type #15681 —RemoteFormInputtype rejectsnullandundefined - Missing field for form doesn't report error #14990 — Missing field for form doesn't report error (exactly Direction 2)
- Make
form.fieldsarray proxies iterable #15833 — Array proxies not iterable - Reactive remote form field proxy collection #14647 — Can't enumerate form field proxies
as("hidden")should allow omitting the value when the field manages its own state #15887 —as("hidden")requires redundant explicit value- Field value retrieved from
form()is initially empty #14676 — Field value initially empty - Select Elements Don't Update When Value Changes #14751 — Select elements don't update when value changes
<select>needs two clicks when used with remote functions form #15835 —<select>needs two clicks with remote form- feat: remote form factory #14815 — Remote form factory with
initialData(open 8 months, 27👍/18❤️) - feat: Reset form programmatically #14779 — Programmatic
form.reset()(open 8 months, 29❤️/28🚀)
Related open PRs with active maintainer engagement
- feat: set value of
submitfields when form is submitted #15979 — Rich Harris (DRAFT): set submit field values - feat: return
datato form enhance callback function #15970 — ottomated: return data to enhance callback - fix: make form fields.value() reflect input_value passed to .as() #15690 — Make
value()reflectinput_valuefrom.as()(stalled on SSR concerns)
v3 milestone
The v3 milestone is ~56% complete (49 open / 63 closed) but contains no remote form issues or PRs. Given that elliott acknowledged "this points at some larger issue we need to solve around how the 'initial state' of forms works" (#15690, Apr 21), and Rich Harris is actively working on form PRs (#15979):
Is there a plan to address the state/DOM divergence as part of v3? If so, the dev-mode check could serve as a bridge. If not, it would provide permanent protection against this class of bugs.
Questions for maintainers
- Is the
value()/ FormData divergence considered a bug, or intentional design? (The stalled PR fix: make form fields.value() reflect input_value passed to .as() #15690 suggests it's acknowledged but unresolved.) - Would a dev-mode consistency check be welcome as a PR, or is there a broader refactor planned that would make it obsolete?
- Should
form()eventually guarantee thatfields.value()always reflects what would be submitted — making it the single source of truth? This would also allowvalue()to returnTinstead ofDeepPartial<T>.
Activity
Nic-Polumeyv commentedon Jul 18, 2026
#15908 and #16359 look like live instances of Direction 1 here, the submitter button's value in one case and untouched or programmatically set fields in the other, both present in the submitted FormData but never entering the fields state.