-
-
Notifications
You must be signed in to change notification settings - Fork 667
fix(vue-form): generate an SSR-safe default formId #2259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
harshit-d3v
wants to merge
2
commits into
TanStack:main
Choose a base branch
from
harshit-d3v:fix/vue-form-ssr-safe-form-id
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+106
β1
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@tanstack/vue-form': patch | ||
| --- | ||
|
|
||
| Generate the default `formId` with Vue's `useId` so it is SSR-safe, falling back to a random uuid on `vue@3.4` (where `useId` does not exist) and outside of a component instance. | ||
|
|
||
| Fixes #2254 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import * as Vue from 'vue' | ||
| import { uuid } from '@tanstack/form-core' | ||
|
|
||
| /** | ||
| * `useId` was added in vue@3.5, but this package supports `vue@^3.4.0`. It is | ||
| * read off the namespace, typed as optional, rather than imported by name: a | ||
| * named import would make bundlers fail to resolve it on 3.4. | ||
| * Read more: https://github.com/webpack/webpack/issues/14814 | ||
| */ | ||
| const vueUseId: { useId?: () => string } = Vue | ||
|
|
||
| /** | ||
| * Returns an SSR-safe id for the form, so the server-rendered markup and the | ||
| * client hydration agree on it. Falls back to a random uuid when `useId` is | ||
| * unavailable, or when called outside of a component instance, where `useId` | ||
| * cannot produce a stable value. | ||
| */ | ||
| export function useFormId(): string { | ||
| const useId = vueUseId.useId | ||
| if (useId && Vue.getCurrentInstance()) { | ||
| return useId() | ||
| } | ||
| return uuid() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import { render } from '@testing-library/vue' | ||
| import { createSSRApp, defineComponent, h, nextTick } from 'vue' | ||
| import { renderToString } from 'vue/server-renderer' | ||
| import { useForm } from '../src' | ||
|
|
||
| const Comp = defineComponent(() => { | ||
| const form = useForm({ defaultValues: { firstName: '' } }) | ||
|
|
||
| return () => <form id={form.formId} data-testid="form" /> | ||
| }) | ||
|
|
||
| describe('useFormId', () => { | ||
| it('uses the provided formId when one is given', () => { | ||
| const WithId = defineComponent(() => { | ||
| const form = useForm({ | ||
| 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('generates the same default formId across identical renders', () => { | ||
| // Scoped to each render's own container: both mount into `document.body`, | ||
| // so a document-wide query would find each other's form too. | ||
| const first = render(Comp).container.querySelector('form')?.id | ||
| const second = render(Comp).container.querySelector('form')?.id | ||
|
|
||
| expect(first).toBeTruthy() | ||
| expect(second).toBe(first) | ||
| }) | ||
|
|
||
| it('generates a default formId that matches between server and client', async () => { | ||
| const html = await renderToString(createSSRApp(Comp)) | ||
| // `\s` so this doesn't match the `id="` inside `data-testid="..."` | ||
| const serverId = html.match(/\sid="([^"]*)"/)?.[1] | ||
|
|
||
| const clientId = render(Comp).getByTestId('form').getAttribute('id') | ||
|
|
||
| expect(serverId).toBeTruthy() | ||
| expect(clientId).toBe(serverId) | ||
| }) | ||
|
|
||
| it('hydrates server-rendered markup without a mismatch', async () => { | ||
| const container = document.createElement('div') | ||
| container.innerHTML = await renderToString(createSSRApp(Comp)) | ||
| document.body.appendChild(container) | ||
|
|
||
| const serverId = container.querySelector('form')?.id | ||
|
|
||
| // Vue reports hydration mismatches through `console.error`, so actually | ||
| // hydrate the server markup and assert none were raised. | ||
| const errors: Array<string> = [] | ||
| const spy = vi | ||
| .spyOn(console, 'error') | ||
| .mockImplementation((...args) => void errors.push(args.join(' '))) | ||
|
|
||
| createSSRApp(Comp).mount(container) | ||
| await nextTick() | ||
| spy.mockRestore() | ||
|
|
||
| expect(errors.filter((e) => /hydration/i.test(e))).toEqual([]) | ||
| expect(container.querySelector('form')?.id).toBe(serverId) | ||
| }) | ||
| }) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
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:
onServerPrefetchcausing unstable ids fromuseId, leading to hydration mismatches Β vuejs/core#12102useIdcause hydration issue if treeshaken client-sideΒ vuejs/core#12591Vue 3.4 supports still generate SSR/client mismatched form IDs.
useId()is unavailable in Vue 3.4, so package authors using@tanstack/vue-formwith the supported range fall back touuid()and can get different server/clientformIds. 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