radix-vue / radix-vue

Vue port of Radix UI Primitives. An open-source UI component library for building high-quality, accessible design systems and web apps.

Home Page:https://radix-vue.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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

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>

commented

@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.