[Bug]: useId composable generate different id on client and server for SSR rendering
mykhailo-alekseiev opened this issue · comments
Environment
Developement/Production OS: MacOs Sonoma 14.4.1
Node version: 20.10.0
Package manager: bun@1.1.7
Radix Vue version: 1.7.4
Vue version: 3.4.27
Nuxt version: 3.11.2
Nuxt mode: universal
Nuxt target: server
CSS framework: tailwindcss (nuxt module)
Client OS: MacOs Sonoma 14.4.1
Browser: Arc Browser 124.0.6367.155
Steps to reproduce
- initialize Nuxtjs project with Shadcn ui library
- install form and input components from there
- Use the example of the Form component
Describe the bug
Hi!
I caught the issue with hydration and radix. I implemented the fix with the ConfigProvider component and used composable but for input components, I bumped into a warning that
[Vue warn]: Hydration attribute mismatch on <input class="flex h-12 w-full rounded-[.875rem] border border-transparent bg-neutral-50 px-3.5 py-3 text-base text-primary-black ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-normal placeholder:text-neutral-400 autofill:!bg-neutral-50 focus-visible:border-primary-black focus-visible:outline-none disabled:cursor-not-allowed" data-v-inspector="components/ui/input/Input.vue:24:3" type="text" placeholder="username" name="username" id="radix-nE3KxD8SGyR-1-form-item" aria-describedby="radix-nE3KxD8SGyR-1-form-item-description" aria-invalid="false" value>flex
- rendered on server: id="radix-nE3KxD8SGyR-1-form-item"
- expected on client: id="radix-nE3KxD8SGyR_1-form-item"
Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead.
You should fix the source of the mismatch.
at <Input type="text" placeholder="username" name="username" ... >
at <PrimitiveSlot id="radix-nE3KxD8SGyR_1-form-item" aria-describedby="radix-nE3KxD8SGyR_1-form-item-description" aria-invalid=false >
at <FormControl>
at <FormItem>
at <Field name="username" >
Packages:
"dependencies": {
"@nuxt/eslint": "0.3.12",
"@nuxt/fonts": "0.7.0",
"@nuxt/image": "1.7.0",
"@nuxtjs/html-validator": "1.8.1",
"@nuxtjs/i18n": "8.3.1",
"@nuxtjs/tailwindcss": "6.12.0",
"@vee-validate/zod": "4.12.8",
"@vueuse/core": "^10.9.0",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"lucide-vue-next": "0.378.0",
"nuxt": "3.11.2",
"nuxt-graphql-client": "0.2.34",
"nuxt-svgo": "4.0.1",
"nuxt-time": "0.1.3",
"radix-vue": "1.7.4",
"shadcn-nuxt": "0.10.4",
"tailwind-merge": "2.3.0",
"tailwindcss-animate": "1.0.7",
"vee-validate": "4.12.8",
"vue": "3.4.27",
"vue-router": "4.3.2",
"zod": "3.23.8"
},
"devDependencies": {
"@vueuse/nuxt": "^10.9.0",
"locize-cli": "^8.0.1",
"typescript": "5.4.5",
"vue-tsc": "2.0.17"
}
Expected behavior
Client and server IDs have to be the same.
Context & Screenshots (if applicable)
![Screenshot 2024-05-13 at 23 59 29](https://private-user-images.githubusercontent.com/47672736/330203361-f265de9c-a438-4264-a54e-3e6462594a0e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjIzMTkyODksIm5iZiI6MTcyMjMxODk4OSwicGF0aCI6Ii80NzY3MjczNi8zMzAyMDMzNjEtZjI2NWRlOWMtYTQzOC00MjY0LWE1NGUtM2U2NDYyNTk0YTBlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA3MzAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNzMwVDA1NTYyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThlMTU5NTcxYTcwOWRlZjJhYmE0YmFlZjQzYWVmMjI2YzAyMjYyNDYyOWI5NDE2ZDlhODExMzZmNDc1ZWYyNjYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.kIH40vgVQXtdFuqQ62WTBuA7-4zt-EMzOSzQQGaajlk)
Code samples:
app.vue:
<template>
<ConfigProvider :use-id="useIdFunction">
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</ConfigProvider>
</template>
<script lang="ts" setup>
import { ConfigProvider } from 'radix-vue'
const route = useRoute()
const useIdFunction = () => useId()
const appConfig = useAppConfig()
const { t } = useI18n()
useHead({
title: () => t(route.meta.title as string) || '',
titleTemplate: title => (title ? `${title} - ${appConfig.title}` : appConfig.title),
bodyAttrs: { class: 'font-inter' },
})
</script>
input.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { useVModel } from '@vueuse/core'
import { cn } from '~/lib/utils'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
isError?: boolean
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input
v-model="modelValue"
:class="cn('flex h-12 w-full rounded-[.875rem] border border-transparent bg-neutral-50 px-3.5 py-3 text-base text-primary-black ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-normal placeholder:text-neutral-400 autofill:!bg-neutral-50 focus-visible:border-primary-black focus-visible:outline-none disabled:cursor-not-allowed',
props.class,
isError && 'border-system-red focus-visible:border-system-red')"
>
</template>
Form.vue
<template>
<form @submit="onSubmit">
<div
class="grid w-full gap-1"
>
<FormField
v-slot="{ componentField, errorMessage }"
name="username"
>
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
type="text"
placeholder="username"
v-bind="componentField"
:disabled="form.isSubmitting.value"
:is-error="!!errorMessage"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField
v-slot="{ componentField, errorMessage }"
name="password"
>
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="password"
v-bind="componentField"
:disabled="form.isSubmitting.value"
:is-error="!!errorMessage"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
<button />
<Button
type="submit"
full-width
:loading="form.isSubmitting.value"
>
Continue
</Button>
</form>
</template>
<script lang="ts" setup>
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
username: z.string().min(1, {
message: 'required',
}),
password: z.string().min(1, {
message: 'required',
}),
}))
const form = useForm({
validationSchema: formSchema,
initialValues: {
username: '',
password: '',
},
})
const onSubmit = form.handleSubmit(async ({ password, username }) => {
try {
const { password_login } = await GqlPasswordLogin({
password,
username,
})
if (password_login) {
alert('Logged in!')
}
// await router.push('/dashboard')
}
catch (error) {
console.log(error)
}
})
</script>
<style>
</style>
@mykhailo-alekseiev Could I ask how you resolved this issue? I've encountered the same problem where radix-vue generates IDs with a difference between an underscore '_' and a hyphen '-' on the client and server sides.
It occurs with the MenubarTrigger component in radix-vue.