Skip to content

Commit d7fd741

Browse files
harshit-d3vclaude
andauthored
fix(solid-form): generate an SSR-safe default formId (#2263)
* fix(solid-form): generate an SSR-safe default formId `createForm` never passed a `formId` to `FormApi`, so it fell back to `uuid()`, which is seeded from `Math.random()` and therefore differs between the server render and the client render. Binding it (`<form id={form.formId}>`) produced a hydration mismatch under SolidStart. Use Solid's `createUniqueId()` for the default instead, which is what the other adapters already do: - react-form/src/useFormId.ts — React.useId, falling back to useUUID on React 17 - preact-form/src/useFormId.ts — useId from preact/hooks - vue-form/src/useFormId.ts — Vue's useId, added in #2259 An explicitly provided `formId` is still preserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(solid-form): correct changeset wording for the formId fallback An explicitly provided `formId` was already forwarded to `FormApi`; only the fallback was missing. Say that, and capitalize the framework names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 77d6815 commit d7fd741

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

‎.changeset/great-pugs-shave.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/solid-form': patch
3+
---
4+
5+
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 (`<form id={form.formId}>`) 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.

‎packages/solid-form/src/createForm.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FormApi, functionalUpdate } from '@tanstack/form-core'
2-
import { createComputed, onMount } from 'solid-js'
2+
import { createComputed, createUniqueId, onMount } from 'solid-js'
33
import { useSelector } from '@tanstack/solid-store'
44
import { Field, createField } from './createField'
55
import { FormGroup } from './createFormGroup'
@@ -240,6 +240,14 @@ export function createForm<
240240
>,
241241
) {
242242
const options = opts?.()
243+
/**
244+
* `FormApi` falls back to `uuid()` when no `formId` is given, which differs
245+
* between the server render and the client render. `createUniqueId` is
246+
* Solid's SSR-safe counterpart, so binding the id (`<form id={form.formId}>`)
247+
* no longer produces a hydration mismatch. Mirrors `useFormId` in the React,
248+
* Preact, and Vue adapters.
249+
*/
250+
const fallbackFormId = createUniqueId()
243251
const api = new FormApi<
244252
TParentData,
245253
TFormOnMount,
@@ -253,7 +261,7 @@ export function createForm<
253261
TFormOnDynamicAsync,
254262
TFormOnServer,
255263
TSubmitMeta
256-
>(options)
264+
>({ ...options, formId: options?.formId ?? fallbackFormId })
257265
const extendedApi: typeof api &
258266
SolidFormApi<
259267
TParentData,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { render } from '@solidjs/testing-library'
3+
import { createForm } from '../src/index'
4+
5+
function Comp() {
6+
const form = createForm(() => ({ defaultValues: { firstName: '' } }))
7+
return <form id={form.formId} data-testid="form" />
8+
}
9+
10+
describe('createForm formId', () => {
11+
it('uses the provided formId when one is given', () => {
12+
function WithId() {
13+
const form = createForm(() => ({
14+
defaultValues: { firstName: '' },
15+
formId: 'my-form',
16+
}))
17+
return <form id={form.formId} data-testid="form" />
18+
}
19+
20+
const { getByTestId } = render(() => <WithId />)
21+
22+
expect(getByTestId('form').getAttribute('id')).toBe('my-form')
23+
})
24+
25+
it('derives the default formId from Solid so it survives hydration', () => {
26+
// `form-core` falls back to `uuid()`, which is seeded from `Math.random()`
27+
// and so differs between the server render and the client render. Solid's
28+
// `createUniqueId()` is the adapter's SSR-safe counterpart: on the client
29+
// it yields `cl-<n>`, and under hydration it derives the id from the
30+
// hydration context so both renders agree. Asserting the id comes from
31+
// that sequence is what pins the default to the hydration-safe source.
32+
const { getByTestId } = render(() => <Comp />)
33+
34+
expect(getByTestId('form').getAttribute('id')).toMatch(/^cl-\d+$/)
35+
})
36+
})

0 commit comments

Comments
 (0)