From 285f6cd16dc5c5adab73e4051daddee935e7a696 Mon Sep 17 00:00:00 2001 From: okxint Date: Tue, 13 Oct 2026 10:41:56 +0530 Subject: [PATCH 1/2] fix(svelte-form): use stable module-level counter for default formId When formId is not provided, FormApi falls back to uuid(), generating a different ID on the server and client, causing SSR hydration mismatches when form.formId is bound to a DOM attribute in SvelteKit. Add a module-level counter (nextFormId) that increments in component-tree order, matching both SSR and hydration passes deterministically. Mirrors the same pattern used by angular-form (APP_ID + counter, PR #2415). Explicit opts.formId is always respected unchanged. Fixes #2388 --- packages/svelte-form/src/createForm.svelte.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index 1b40ce748..0a5302d22 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,6 +1,12 @@ import { FormApi } from '@tanstack/form-core' import { useSelector } from '@tanstack/svelte-store' import { onMount } from 'svelte' + +let _formCounter = 0 + +function nextFormId(): string { + return `svelte-form-${++_formCounter}` +} import Field from './Field.svelte' import FormGroup from './FormGroup.svelte' import Subscribe from './Subscribe.svelte' @@ -310,7 +316,7 @@ export function createForm< TFormOnDynamicAsync, TFormOnServer, TSubmitMeta - >(options) + >({ ...options, formId: options?.formId ?? nextFormId() } as typeof options) const extendedApi: typeof api & SvelteFormApi< TParentData, From 6dec71a7293aafbbcaa39b61024618145e0e3e04 Mon Sep 17 00:00:00 2001 From: okxint Date: Sun, 18 Oct 2026 14:46:54 +0530 Subject: [PATCH 2/2] fix(svelte-form): scope form id counter to component context A module-level counter persists across SvelteKit SSR requests, so the server counter drifts ahead of the client (which always starts at zero), causing hydration mismatches on forms without explicit ids. Using Svelte getContext/setContext scopes the counter to each component tree, which resets naturally per SSR request. --- packages/svelte-form/src/createForm.svelte.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index 0a5302d22..bf75b4a96 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,11 +1,18 @@ import { FormApi } from '@tanstack/form-core' import { useSelector } from '@tanstack/svelte-store' -import { onMount } from 'svelte' +import { getContext, hasContext, onMount, setContext } from 'svelte' -let _formCounter = 0 +const FORM_ID_CTX = Symbol() function nextFormId(): string { - return `svelte-form-${++_formCounter}` + try { + if (!hasContext(FORM_ID_CTX)) setContext(FORM_ID_CTX, { n: 0 }) + const ctx = getContext<{ n: number }>(FORM_ID_CTX) + return `svelte-form-${++ctx.n}` + } catch { + // Outside component context (unit tests, etc.) + return `svelte-form-${Math.random().toString(36).slice(2, 9)}` + } } import Field from './Field.svelte' import FormGroup from './FormGroup.svelte'