Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-pugs-shave.md
Original file line number Diff line number Diff line change
@@ -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 (`<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.
12 changes: 10 additions & 2 deletions packages/solid-form/src/createForm.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 (`<form id={form.formId}>`)
* no longer produces a hydration mismatch. Mirrors `useFormId` in the React,
* Preact, and Vue adapters.
*/
const fallbackFormId = createUniqueId()
const api = new FormApi<
TParentData,
TFormOnMount,
Expand All @@ -253,7 +261,7 @@ export function createForm<
TFormOnDynamicAsync,
TFormOnServer,
TSubmitMeta
>(options)
>({ ...options, formId: options?.formId ?? fallbackFormId })
const extendedApi: typeof api &
SolidFormApi<
TParentData,
Expand Down
36 changes: 36 additions & 0 deletions packages/solid-form/tests/createFormId.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <form id={form.formId} data-testid="form" />
}

describe('createForm formId', () => {
it('uses the provided formId when one is given', () => {
function WithId() {
const form = createForm(() => ({
defaultValues: { firstName: '' },
formId: 'my-form',
}))
return <form id={form.formId} data-testid="form" />
}

const { getByTestId } = render(() => <WithId />)

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-<n>`, 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(() => <Comp />)

expect(getByTestId('form').getAttribute('id')).toMatch(/^cl-\d+$/)
})
})