diff --git a/.changeset/great-pugs-shave.md b/.changeset/great-pugs-shave.md new file mode 100644 index 000000000..6b7151570 --- /dev/null +++ b/.changeset/great-pugs-shave.md @@ -0,0 +1,5 @@ +--- +'@tanstack/solid-form': patch +--- + +Generate the default `formId` with Solid's `createUniqueId` so it is SSR-safe. Previously, when no `formId` was configured, `createForm` did not provide a fallback, so `FormApi` generated a random UUID that differed between the server render and the client render. Binding that generated id (`
`) produced a hydration mismatch under SolidStart. An explicitly provided `formId` was already forwarded and is unchanged. This mirrors the existing behaviour of the React, Preact, and Vue adapters. diff --git a/packages/solid-form/src/createForm.tsx b/packages/solid-form/src/createForm.tsx index 01a216412..e583bea73 100644 --- a/packages/solid-form/src/createForm.tsx +++ b/packages/solid-form/src/createForm.tsx @@ -1,5 +1,5 @@ import { FormApi, functionalUpdate } from '@tanstack/form-core' -import { createComputed, onMount } from 'solid-js' +import { createComputed, createUniqueId, onMount } from 'solid-js' import { useSelector } from '@tanstack/solid-store' import { Field, createField } from './createField' import { FormGroup } from './createFormGroup' @@ -240,6 +240,14 @@ export function createForm< >, ) { const options = opts?.() + /** + * `FormApi` falls back to `uuid()` when no `formId` is given, which differs + * between the server render and the client render. `createUniqueId` is + * Solid's SSR-safe counterpart, so binding the id (``) + * no longer produces a hydration mismatch. Mirrors `useFormId` in the React, + * Preact, and Vue adapters. + */ + const fallbackFormId = createUniqueId() const api = new FormApi< TParentData, TFormOnMount, @@ -253,7 +261,7 @@ export function createForm< TFormOnDynamicAsync, TFormOnServer, TSubmitMeta - >(options) + >({ ...options, formId: options?.formId ?? fallbackFormId }) const extendedApi: typeof api & SolidFormApi< TParentData, diff --git a/packages/solid-form/tests/createFormId.test.tsx b/packages/solid-form/tests/createFormId.test.tsx new file mode 100644 index 000000000..9b04e9b4a --- /dev/null +++ b/packages/solid-form/tests/createFormId.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { render } from '@solidjs/testing-library' +import { createForm } from '../src/index' + +function Comp() { + const form = createForm(() => ({ defaultValues: { firstName: '' } })) + return +} + +describe('createForm formId', () => { + it('uses the provided formId when one is given', () => { + function WithId() { + const form = createForm(() => ({ + defaultValues: { firstName: '' }, + formId: 'my-form', + })) + return + } + + const { getByTestId } = render(() => ) + + expect(getByTestId('form').getAttribute('id')).toBe('my-form') + }) + + it('derives the default formId from Solid so it survives hydration', () => { + // `form-core` falls back to `uuid()`, which is seeded from `Math.random()` + // and so differs between the server render and the client render. Solid's + // `createUniqueId()` is the adapter's SSR-safe counterpart: on the client + // it yields `cl-`, and under hydration it derives the id from the + // hydration context so both renders agree. Asserting the id comes from + // that sequence is what pins the default to the hydration-safe source. + const { getByTestId } = render(() => ) + + expect(getByTestId('form').getAttribute('id')).toMatch(/^cl-\d+$/) + }) +})