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
相关产品推荐
相关产品推荐

