Skip to content

fix(vue-form): generate an SSR-safe default formId - #2259

Open
harshit-d3v wants to merge 2 commits into
TanStack:mainfrom
harshit-d3v:fix/vue-form-ssr-safe-form-id
Open

fix(vue-form): generate an SSR-safe default formId#2259
harshit-d3v wants to merge 2 commits into
TanStack:mainfrom
harshit-d3v:fix/vue-form-ssr-safe-form-id

Conversation

@harshit-d3v

@harshit-d3v harshit-d3v commented Jul 29, 2026

Copy link
Copy Markdown

🎯 Changes

Fixes #2254.

FormApi falls back to uuid() when no formId is passed, and the Vue adapter never passes one — so the id differs between the server render and the client render, and binding it (<form :id="form.formId">) produces a hydration mismatch.

This uses Vue's useId for the default instead, which is exactly what the other adapters already do:

  • react-form/src/useFormId.tsReact.useId, falling back to useUUID on React 17
  • preact-form/src/useFormId.tsuseId from preact/hooks

Vue had no equivalent, so packages/vue-form/src/useFormId.ts adds one and useForm now passes formId: opts?.formId ?? fallbackFormId, mirroring react-form/src/useForm.tsx.

The version constraint

useId was added in vue@3.5, but @tanstack/vue-form declares peerDependencies: { "vue": "^3.4.0" }. So a straight import { useId } from 'vue' would break 3.4 consumers — and a named import of a missing export is a bundler resolution error, not a runtime undefined. It is therefore read off the namespace and typed as optional, with the same reasoning as the existing React comment (webpack/webpack#14814):

const vueUseId: { useId?: () => string } = Vue

export function useFormId(): string {
  const useId = vueUseId.useId
  if (useId && Vue.getCurrentInstance()) {
    return useId()
  }
  return uuid()
}

The getCurrentInstance() guard matters too: outside a component instance useId cannot produce a stable value and warns in dev, so useForm called outside setup keeps the old uuid behaviour rather than emitting a warning.

If you would rather bump the peer range to ^3.5.0 and call useId unconditionally, happy to switch — this way keeps 3.4 working.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Verification

packages/vue-form/tests/useFormId.test.tsx covers this. Run against main with only the test applied (i.e. reverting useForm.tsx), it fails on the two default-id cases while the explicit-id control still passes:

Test Without the fix With the fix
uses the provided formId when one is given ✅ (control)
same default formId across identical renders 2902fb86-… vs 752902fb-…
default formId matches between server and client fb8639db-… vs 02fb8639-…

The server/client case renders through renderToString from vue/server-renderer (reached via the vue subpath export, so it adds no new dependency) and compares against a client render.

Also run locally:

  • pnpm test:prpasses (test:sherif, test:knip, test:docs, test:eslint, test:lib, test:types, test:build, build)
  • full @tanstack/vue-form suite → 31 passed, no type errors

Disclosure: I used an AI assistant while investigating and preparing this change, including running the verification above.

Summary by CodeRabbit

  • New Features
    • Added a new useFormId composable for generating SSR-safe, stable form identifiers.
    • useForm now defaults to an auto-generated formId when none is provided.
  • Bug Fixes
    • Prevented mismatches between server-rendered and client-hydrated form IDs.
    • Ensured safe ID generation when useId isn’t available (including non-component usage).
  • Tests
    • Added Vitest coverage for provided IDs, deterministic defaults, and SSR/client consistency (including hydration without warnings).

FormApi falls back to `uuid()` when no formId is supplied, so the Vue
adapter produced a different id on the server than on the client and
binding it (e.g. `<form :id="form.formId">`) caused a hydration mismatch.

Use Vue's `useId` for the default instead, mirroring what react-form and
preact-form already do. `useId` only exists in vue@3.5 and this package
supports vue@^3.4.0, so it is read off the namespace and falls back to
`uuid()` when unavailable -- and likewise outside a component instance,
where `useId` cannot produce a stable value.

Fixes TanStack#2254.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9914d3e4-6256-49f6-8fac-5247e5f52502

📥 Commits

Reviewing files that changed from the base of the PR and between 3707161 and f3474c7.

📒 Files selected for processing (1)
  • packages/vue-form/tests/useFormId.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue-form/tests/useFormId.test.tsx

📝 Walkthrough

Walkthrough

Vue form defaults now use a component-aware, SSR-safe identifier composable with UUID fallback. useForm preserves explicit IDs, and tests cover deterministic rendering, SSR/client consistency, and hydration.

Changes

Vue form identifier flow

Layer / File(s) Summary
Identifier generation and form integration
packages/vue-form/src/useFormId.ts, packages/vue-form/src/useForm.tsx
Adds Vue useId-based form IDs with UUID fallback, and passes the generated ID to FormApi when no explicit formId is provided.
Identifier validation and release metadata
packages/vue-form/tests/useFormId.test.tsx, .changeset/lucky-donkeys-shout.md
Tests explicit IDs, stable repeated renders, SSR/client consistency, and hydration, and adds patch-release metadata.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VueComponent
  participant useForm
  participant useFormId
  participant FormApi
  VueComponent->>useForm: create form
  useForm->>useFormId: request default formId
  useFormId->>VueComponent: use Vue useId or UUID fallback
  useForm->>FormApi: initialize with formId
  FormApi-->>VueComponent: expose form.formId
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: making the Vue formId default SSR-safe.
Description check ✅ Passed The PR description includes all template sections, checklist items, release impact, and verification details.
Linked Issues check ✅ Passed The code addresses #2254 by generating a stable default formId with useId and falling back for Vue 3.4/component limits.
Out of Scope Changes check ✅ Passed The changes stay focused on the SSR-safe formId fix, tests, and the required changeset.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/vue-form/tests/useFormId.test.tsx (1)

39-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise actual hydration, not only independent renders.

This compares SSR HTML with a new client render, but never mounts a client app onto the server markup. Hydrate the server-rendered container and assert the ID remains unchanged without hydration warnings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-form/tests/useFormId.test.tsx` around lines 39 - 47, Update the
test around the SSR render and client ID assertion to hydrate a Vue app onto the
server-rendered markup instead of independently calling render(Comp). Mount the
client app with hydration enabled using the existing server HTML container,
capture or assert that no hydration warnings occur, and verify the hydrated form
element retains the server-generated ID.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vue-form/src/useFormId.ts`:
- Around line 18-23: Update useFormId so supported Vue 3.4 environments cannot
produce different server/client IDs: either raise the package’s Vue peer/support
requirement to Vue 3.5+ and rely on vueUseId.useId, or replace the uuid()
fallback with a deterministic SSR-aware ID generator while preserving the
existing useId path.

---

Nitpick comments:
In `@packages/vue-form/tests/useFormId.test.tsx`:
- Around line 39-47: Update the test around the SSR render and client ID
assertion to hydrate a Vue app onto the server-rendered markup instead of
independently calling render(Comp). Mount the client app with hydration enabled
using the existing server HTML container, capture or assert that no hydration
warnings occur, and verify the hydrated form element retains the
server-generated ID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f6168d0-4522-4573-8a99-8b17fdc2589f

📥 Commits

Reviewing files that changed from the base of the PR and between 5d11281 and 3707161.

📒 Files selected for processing (4)
  • .changeset/lucky-donkeys-shout.md
  • packages/vue-form/src/useForm.tsx
  • packages/vue-form/src/useFormId.ts
  • packages/vue-form/tests/useFormId.test.tsx

Comment on lines +18 to +23
export function useFormId(): string {
const useId = vueUseId.useId
if (useId && Vue.getCurrentInstance()) {
return useId()
}
return uuid()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)useFormId\.ts$|package\.json$|packages/vue-form|vue-useId|useId|useUUID' || true

echo "== useFormId =="
if [ -f packages/vue-form/src/useFormId.ts ]; then
  cat -n packages/vue-form/src/useFormId.ts
fi

echo "== package manifests relevant =="
for f in package.json packages/vue-form/package.json; do
  if [ -f "$f" ]; then
    echo "-- $f"
    jq '{name,version,type,scripts,peerDependencies,dependencies,devDependencies,engines}' "$f"
  fi
done

echo "== references to useFormId =="
rg -n "useFormId|useid|useId|uuid\\(|vueUseId|Vue\\.use" -S .

Repository: TanStack/form

Length of output: 11353


🌐 Web query:

Vue 3.4 useId not exported composition api helper useId

💡 Result:

The useId composable was not exported or available in Vue 3.4 [1][2][3]. It was introduced as a new API in Vue 3.5 [2][3]. If you are using Vue 3.4 and attempt to import useId from 'vue', it will result in an error indicating that the export cannot be found [4]. To use this functionality, you must upgrade your project to Vue 3.5 or later [2][3]. In versions prior to 3.5, developers typically used custom implementations or relied on utilities provided by third-party libraries (such as Nuxt or Quasar) to generate unique IDs [2].

Citations:


🌐 Web query:

Vue 3.5 useId SSR stable server client ID documentation

💡 Result:

Vue 3.5 introduced the useId composable to generate unique, stable IDs that remain consistent between server-side rendering (SSR) and client-side hydration [1][2]. This feature addresses long-standing challenges in SSR, such as hydration mismatches caused by inconsistent ID generation [1][3]. Key features and behavior: - Stability: useId produces IDs that are guaranteed to match between server and client renders [1][4]. It achieves this by tracking the component tree structure and the positions of asynchronous boundaries (e.g., async components, async setup, and serverPrefetch) to ensure counters are synchronized [4][2]. - Usage: The API is straightforward to implement, requiring no arguments to return a unique string ID [1][3]: <script setup> import { useId } from 'vue' const id = useId </script> label:for="id"Name: <input:id="id" type="text" /> - Customization: IDs are unique per application instance [4]. By default, they use a prefix of v- (e.g., v:1-2-3), which can be customized at the application level using app.config.idPrefix [4][3]. Important considerations: - Async Boundaries: While useId is designed to be robust, edge cases involving asynchronous logic—such as the use of onServerPrefetch or treeshaking patterns in frameworks like Nuxt—have historically caused hydration issues when the server and client treat async boundaries differently [5][6]. These issues are tracked in the Vue core repository and are typically addressed by ensuring the component tree structure remains identical across environments [5][6]. - Legacy Alternatives: Before Vue 3.5, libraries like Nuxt and Quasar implemented their own ID generation strategies to handle SSR [2]. If you are using Vue 3.5 or later, the built-in useId is the recommended approach to replace those custom solutions [2].

Citations:


Vue 3.4 supports still generate SSR/client mismatched form IDs.

useId() is unavailable in Vue 3.4, so package authors using @tanstack/vue-form with the supported range fall back to uuid() and can get different server/client formIds. Either raise the Vue 3.4 support edge to Vue 3.5+ for the safe default, or replace the random UUID fallback with a deterministic SSR-aware implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-form/src/useFormId.ts` around lines 18 - 23, Update useFormId so
supported Vue 3.4 environments cannot produce different server/client IDs:
either raise the package’s Vue peer/support requirement to Vue 3.5+ and rely on
vueUseId.useId, or replace the uuid() fallback with a deterministic SSR-aware ID
generator while preserving the existing useId path.

The server/client case compared SSR html against a fresh client render,
which never exercises hydration itself. Mount the server markup with
`createSSRApp().mount()` and assert Vue raises no hydration mismatch.
@harshit-d3v

Copy link
Copy Markdown
Author

Thanks — both points addressed.

On the hydration test (nitpick): fair, the test compared SSR html against a fresh client render and never actually hydrated. f3474c79 mounts the server markup with createSSRApp().mount() and asserts Vue raises no hydration mismatch. It's a stronger test: reverting useForm.tsx now fails 3 of the 4 cases (the new hydration one included) while the explicit-formId control still passes.

On vue@3.4 still producing mismatched ids: that's accurate, and it is a deliberate limitation rather than something the patch can solve. 3.4 has no useId, and Vue exposes nothing else that is stable across the server and client render, so on 3.4 there is no correct id to fall back to — only the existing uuid() behaviour, which this PR leaves untouched. The net effect is 3.5+ gets fixed and 3.4 is no worse than today.

The alternative is bumping peerDependencies.vue to ^3.5.0 and calling useId unconditionally, which fixes it everywhere the package claims to support at the cost of dropping 3.4. That's a maintainer call on the support window, so I kept the compatible option and am happy to switch if you'd prefer the bump.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TanStack Form Vue - default formId is not SSR-safe

1 participant