You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Hook Form+Shadcn-Ui+Zod集成遇id不匹配及label失效问题求助

问题:Next.js中shadcn-ui + React Hook Form + Zod表单出现服务端/客户端ID不匹配及htmlFor失效

使用Zod、zodResolver、React Hook Form和shadcn-ui构建表单,完全参照shadcn-ui官方示例编写极简代码,但反复出现服务端与客户端id不匹配的警告,重装依赖、参考GitHub资源及他人代码均无法解决,同时存在htmlFor标签失效的问题。

可复现代码

'use client'

import React from 'react'
import * as z from "zod"
import { zodResolver } from '@hookform/resolvers/zod' 
import {useForm} from "react-hook-form"
import { useState } from 'react'
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

const formSchema = z.object({
    name:z.string().min(2)
})

type formType = z.infer<typeof formSchema>

function TestForm() {
    const [loading, setLoading] = useState(false)
    const form = useForm<formType>({
        resolver:zodResolver(formSchema),
        defaultValues: {
            name:""
        },
    })

    const onSubmit =async (values:formType) => {
        console.log(values)
    }

    return (
        <div>
            <Form {...form}>
                <form onSubmit={form.handleSubmit(onSubmit)}>
                    <FormField
                        control={form.control}
                        name='name'
                        render={({field}) => (
                            <FormItem>
                                {/* <FormLabel>Name</FormLabel> */}
                                <FormControl>
                                    <Input disabled={loading} placeholder='Test' {...field}/>
                                </FormControl>
                            </FormItem>
                        )}
                    />
                    <Button disabled={loading} type="submit">Submit</Button>
                </form>
            </Form>
        </div>
    )
}

export default TestForm

报错信息

Warning: Prop `id` did not match. Server: ":R5mmmmcq:-form-item" Client: ":Rmqqqpj9:-form-item"
    at input
    at _c (webpack-internal:///(app-pages-browser)/./components/ui/input.tsx:13:11)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-pages-browser)/./components/ui/form.tsx:124:14)
    at div
    at eval (webpack-internal:///(app-pages-browser)/./components/ui/form.tsx:74:11)
    at Controller (webpack-internal:///(app-pages-browser)/./node_modules/react-hook-form/dist/index.esm.mjs:544:37)
    at FormField (webpack-internal:///(app-pages-browser)/./components/ui/form.tsx:29:14)
    at form
    at FormProvider (webpack-internal:///(app-pages-browser)/./node_modules/react-hook-form/dist/index.esm.mjs:180:13)
    at div
    at TestForm (webpack-internal:///(app-pages-browser)/./app/(auth_)/login/test-form/page.tsx:26:82)
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:228:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:325:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:214:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:335:11)
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:228:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:325:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:214:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:335:11)
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:228:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:325:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:214:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:335:11)
    at InnerLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:228:11)
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at LoadingBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:325:11)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at InnerScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:139:9)
    at ScrollAndFocusHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:214:11)
    at RenderFromTemplateContext (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/render-from-template-context.js:15:44)
    at OuterLayoutRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/layout-router.js:335:11)
    at SessionProvider (webpack-internal:///(app-pages-browser)/./node_modules/next-auth/react/index.js:455:24)
    at AuthProvider (webpack-internal:///(app-pages-browser)/./app/(auth_)/providers/AuthProvider.tsx:11:11)
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:54:9)
    at NotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/not-found-boundary.js:62:11)
    at DevRootNotFoundBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/dev-root-not-found-boundary.js:32:11)
    at ReactDevOverlay (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:326:11)
    at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:148:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:77:9)
    at ErrorBoundary (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:104:11)
    at AppRouter (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:396:13)
    at ServerRoot (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/app-index.js:166:11)
    at RSCComponent
    at ReactDevOverlay (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at Root (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/app-index.js:183:11)

解决方案

1. 修复ID不匹配问题

核心原因是代码中嵌套了两层form标签:shadcn-ui的<Form>组件本身已经封装了原生form,手动添加的<form>会导致ID生成逻辑冲突,引发服务端/客户端ID不一致。

修改后的代码:

'use client'

import React from 'react'
import * as z from "zod"
import { zodResolver } from '@hookform/resolvers/zod' 
import { useForm } from "react-hook-form"
import { useState } from 'react'
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

const formSchema = z.object({
    name: z.string().min(2)
})

type formType = z.infer<typeof formSchema>

function TestForm() {
    const [loading, setLoading] = useState(false)
    const form = useForm<formType>({
        resolver: zodResolver(formSchema),
        defaultValues: {
            name: ""
        },
    })

    const onSubmit = async (values: formType) => {
        console.log(values)
    }

    return (
        <div>
            {/* 直接给shadcn-ui的Form组件绑定onSubmit,无需手动嵌套form标签 */}
            <Form {...form} onSubmit={form.handleSubmit(onSubmit)}>
                <FormField
                    control={form.control}
                    name='name'
                    render={({ field }) => (
                        <FormItem>
                            <FormLabel>Name</FormLabel>
                            <FormControl>
                                <Input disabled={loading} placeholder='Test' {...field} />
                            </FormControl>
                        </FormItem>
                    )}
                />
                <Button disabled={loading} type="submit">Submit</Button>
            </Form>
        </div>
    )
}

export default TestForm

2. 修复htmlFor失效问题

htmlFor失效是因为手动嵌套form标签破坏了shadcn-ui Form组件的内部关联逻辑。去掉手动form标签后,FormLabel会自动和Input的ID绑定,无需额外配置。如果仍有问题,可以手动指定ID:

<FormField
    control={form.control}
    name='name'
    id="name-input"
    render={({ field }) => (
        <FormItem>
            <FormLabel htmlFor="name-input">Name</FormLabel>
            <FormControl>
                <Input disabled={loading} placeholder='Test' {...field} id="name-input" />
            </FormControl>
        </FormItem>
    )}
/>

其他排查点

  • 确认依赖版本兼容:确保react-hook-form >=7.45.0、zod >=3.0.0、@hookform/resolvers版本与前两者匹配
  • 清除Next.js缓存:运行next dev --clean或删除.next文件夹后重启项目
  • 确保组件纯客户端渲染:虽然已加'use client',但需确认父组件没有服务端渲染冲突

内容的提问来源于stack exchange,提问作者bdthombre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 09:43:09