与watch不同,getValues方法不会触发页面rerender,也不订阅input changes。
当用户点击按钮或者做特定action时,使用getValues方法是比watch更好的选择。
const { getValues } = useForm()

1、获取全部form values
x1import { useForm, useFieldArray } from "react-hook-form";2import { DevTool } from '@hookform/devtools'3import { useEffect } from "react";45export const YoutubeForm = () => {67 const form = useForm<formValues>({8 defaultValues: {9 10 }11 });12 13 // 解构出getValues方法14 const { register, control, handleSubmit, formState, watch, getValues } = form;15 const { errors } = formState;1617 // 使用button的点击事件,来触发getValues方法18 const handleGetValues = () => {19 console.log("get values", getValues());20 }2122 renderCount++23 return (24 <div>25 <form onSubmit={handleSubmit(onSubmit)}>26 .........2728 <button>Submit </button>29 30 {/* 使用一个button来触发getValues方法 */}31 <button type="button" onClick={handleGetValues}>get Form values</button>32 </form>33 <DevTool control={control} />34 </div>35 );36};可以看到,input的change事件并不会触发getValues事件。

2、获取一个form value
xxxxxxxxxx51// 传递一个fieldname,就可以获取这个fieldvalue23const handleGetValues = () => {4 console.log("get values : ", getValues("username"));5}
3、获取多个form values
xxxxxxxxxx51// 传递一个fieldnames数组,就可以获取这个数组里参数对应的fieldvalue23const handleGetValues = () => {4 console.log("get values : ", getValues(["username", "email"]));5}
setValue方法可以让我们设置已经registered的filedvalue,通过程序的方法来设置。

xxxxxxxxxx351import { useForm, useFieldArray } from "react-hook-form";2import { DevTool } from '@hookform/devtools'3import { useEffect } from "react";45type formValues = {6 7}89export const YoutubeForm = () => {1011 const form = useForm<formValues>({12 defaultValues: {13 14 }15 });16 17 // 从useForm中解构出 setValue18 const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;1920 const handleSetValue = () => {21 // 第一个参数是fieldname,第二个参数是想要修改为的值22 setValue("username", "")23 }2425 renderCount++26 return (27 <div>28 <form onSubmit={handleSubmit(onSubmit)}>29 ......30 {/*使用button点击事件来触发setValue*/}31 <button type="button" onClick={handleSetValue}>Set value</button>32 </form>33 </div>34 );35};可以看到,username的值变为空:

设置值成功了,但是devtools里面的touched这些都没有变化,说明此时react hook form是没有把它当作用户交互的,里面可以配置。配置了之后,就可以给出提示,看设置值是否符合需求。
xxxxxxxxxx71const handleSetValue = () => {2 setValue("username", "", {3 shouldDirty: true,4 shouldTouch: true,5 shouldValidate: true6 })7}再来触发,就可以看到devtools里面变化了。

devtools里面的Touched,是一个boolean值,表示用户是否交互过。Dirty是一个boolean值,表示用户是否修改过值(是与default value来比较,值是否修改过)。
可以从useForm里面的formState解构出整个form的touched和dirty。
xxxxxxxxxx41const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;2const { errors, touchedFields, dirtyFields } = formState;34console.log({ touchedFields, dirtyFields });可以看到,输出的值是对象,里面表示哪些field交互或者修改了。

另外formState里面还有一个isDirty属性,用来表示整个form是否被修改过。
xxxxxxxxxx41const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;2const { errors, touchedFields, dirtyFields, isDirty } = formState;34console.log({ touchedFields, dirtyFields, isDirty });可以看到,如果form中的任一项被修改了,isDirty都会变为true。

isDirty可以用于enable submit button,只有当用户修改了数据之后,才能点击按钮。
传统写法,如果要设置disable一个input,那么需要在input上设置它的disabled属性。
在react hook form中,通过设置register的第二个参数中,添加disabled: true来disable一个input。
xxxxxxxxxx61<div className="form-control">2 <label htmlFor="twitter"> Twitter </label>3 <input type="text" id="twitter" {register("social.twitter", {4 disabled: true,5 })} />6</div>需要注意的是,当使用react hook form的方式diasble一个input之后,这个input的值会变为undefined,而且它的校验也失效了。
xxxxxxxxxx111<div className="form-control">2 <label htmlFor="twitter"> Twitter </label>3 <input type="text" id="twitter" {register("social.twitter", {4 disabled: true,5 required: {6 value: true,7 message: "Twitter is required"8 }9 })} />10 <p className="error">{errors.social?.twitter?.message}</p>11</div>上面的代码里面,设置了disabled,也设置了required,那么required会生效吗?填写表单里面的其它信息,提交表单看一下。

可以看到,不会出现error信息,输出的social里面也没有twitter,说明twitter此时的值是undefined。
这是react hook form的现象,记住有这种现象即可,一般我都不会把一个必填项设置为静态的disabled,一般来说都是根据用户的输入来决定。比如说这里设置,当channel字段为空时,disabled才为true。
xxxxxxxxxx111<div className="form-control">2 <label htmlFor="twitter"> Twitter </label>3 <input type="text" id="twitter" {register("social.twitter", {4 disabled: watch("channel") === "",5 required: {6 value: true,7 message: "Twitter is required"8 }9 })} />10 <p className="error">{errors.social?.twitter?.message}</p>11</div>
之前的案例中,在form的onSubmit上面,绑定的是handleSubmit(onSubmit),其中handleSubmit是从useForm解构出来的,onSubmit是自己定义的。那么为什么不直接绑定onSubmit呢?因为我看到handleSubmit也没有做额外的事情啊。
这是因为handleSubmit除了可以处理提交之外,还可以处理提交遇到的错误。

xxxxxxxxxx11import { type FieldErrors } from "react-hook-form";xxxxxxxxxx371import { useForm, useFieldArray, type FieldErrors } from "react-hook-form";2import { DevTool } from '@hookform/devtools'3import { useEffect } from "react";45let renderCount = 0;67type formValues = {8 9}1011export const YoutubeForm = () => {1213 const form = useForm<formValues>({14 defaultValues: {15 16 }17 });18 const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;19 const { errors, touchedFields, dirtyFields, isDirty } = formState;2021 const onSubmit = (data: formValues) => {22 console.dir(data);23 }2425 // 定义onError,这里会接收到一个参数errors,就是提交时报错的信息26 const onError = (errors: FieldErrors<formValues>) => {27 console.log("Form errors : ", errors);28 }2930 return (31 <div>32 <form onSubmit={handleSubmit(onSubmit, onError)}>33 ......34 </form>35 </div>36 );37};效果:

这个submission error看上去不怎么样,其实我觉得逻辑非常清晰,这里就是专门用来获取validation相关的报错信息的,和接口请求是分开的,如果这里有报错,那么
handleSubmit的第一个参数onSubmit就不会执行,逻辑很清除。我之前在vue里面都是写成一大堆,逻辑都很杂乱。使用这个submission error,可以获取到message信息,这样就可以使用messagebox的方式来提示用户。
有时候如果表单没有填写一些必填项,那么我想让submit button处于disabled的状态,这时候就可以使用watch来监听,<button disabled={watch("username") === "" || watch("email") === ""}>Submit</button>。
如果是要用户修改表单任意一项之后,submit button才能点击,那么可以使用isDirty来判断。
xxxxxxxxxx101const form = useForm<formValues>({2 defaultValues: {3 4 }5});6const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;7const { errors, touchedFields, dirtyFields, isDirty } = formState;8910<button disabled={!isDirty}>Submit </button>可以看到,当表单任意一项更改之后,submit button才能点击。

还有isValid可以用来监听form是否通过校验,只有通过校验之后才能点击。
xxxxxxxxxx101const form = useForm<formValues>({2 defaultValues: {3 4 }5});6const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;7const { errors, touchedFields, dirtyFields, isDirty, isValid } = formState;8910<button disabled={!isValid}>Submit </button>这里我有一个疑问,就是之前说过,设置的校验规则只能在submit时触发,如果是第一次填写表单,填写完成了,都符合要求了,这时候的submit button还是disabled状态吗?
这取决于
isValid此时是什么值,可以看一下下面的动图,从useForm获取到的formState的相关值都是实时更新的,所以上面的问题是不成立的。如果都填好了,都符合要求了,那么此时的isValid为true。

formState里面有很多状态,前面学习了errors、touchedFields、dirtyFields、isDirty、isValid。这节课来学习form提交相关的状态。
全部状态可以查看官方文档:https://react-hook-form.com/docs/useform/formstate。

这几个状态的含义如下:

xxxxxxxxxx91const form = useForm<formValues>({2 defaultValues: {3 4 }5});6const { register, control, handleSubmit, formState, watch, getValues, setValue } = form;7const { errors, touchedFields, dirtyFields, isDirty, isValid, isSubmitting, isSubmitSuccessful, isSubmitted, submitCount } = formState;89console.log({ isSubmitSuccessful, isSubmitting, isSubmitted, submitCount });输出看一下:

其中有用的就是isSubmitting,可以加到submit button上面去,避免用户多次点击。
xxxxxxxxxx11<button disabled={!isValid || isSubmitting}>Submit </button>当用户填写完成之后、或者用户刚进入页面之后,我们需要将form设置成初始状态,这时候可以使用useForm提供的reset。reset方法是重置整个表单。
xxxxxxxxxx11const { reset } = useForm();xxxxxxxxxx291import { useForm, useFieldArray, type FieldErrors } from "react-hook-form";2import { DevTool } from '@hookform/devtools'3import { useEffect } from "react";45let renderCount = 0;67type formValues = {8 9}1011export const YoutubeForm = () => {1213 const form = useForm<formValues>({14 defaultValues: {15 16 }17 });18 // 从useForm里面解构出 reset19 const { register, control, handleSubmit, formState, watch, getValues, setValue, reset } = form;2021 return (22 <div>23 <form onSubmit={handleSubmit(onSubmit, onError)}>24 {/* 设置reset按钮 */}25 <button type="button" onClick={() => reset()}>Reset</button>26 </form>27 </div>28 );29};将表单填满后,点击reset:

一般不通过点击按钮的方式来reset,而是使用useEffect。
xxxxxxxxxx51useEffect(() => {2 if (isSubmitSuccessful) {3 reset()4 }5}, [isSubmitSuccessful, reset])表单填好之后,点击提交,看是否会清空表单:

可以看到,提交成功之后,就触发了reset。
案例:在用户输入email时,校验是否存在同样的email地址,使用接口https://jsonplaceholder.typicode.com/users?email=Sincere@april.biz来校验,这时候校验就要等到接口数据返回之后才能继续,所以这里需要使用async校验。

需要在email的register里面,新增一个校验函数。
xxxxxxxxxx281<div className="form-control">2 <label htmlFor="email"> Email </label>3 <input type="text" id="email" {register("email", {4 pattern: {5 value: /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,6 message: "Invalid email format"7 },8 validate: {9 // 校验某个特殊的邮箱地址10 notAdmin: (fieldValue) => {11 return fieldValue !== "admin@example.com" || "Enter a different email address"12 },13 // 校验邮箱是否在黑名单中14 notBlacklisted: (fieldValue) => {15 return !fieldValue.endsWith("baddomain.com") || "This domain is not supported"16 },17 18 // 校验邮箱是否已存在19 emailAvailable: async (filedValue) => {20 const response = await fetch(`https://jsonplaceholder.typicode.com/users?email=${filedValue}`)21 const data = await response.json()2223 return data.length === 0 || "Email already exists"24 }25 }26 })} />27 <p className="error">{errors.email?.message}</p>28</div>可以看到,在校验通过之前,是不会提交的。

这个会不会造成性能问题,就是用户输入一点,就校验一次?在emailAvailable里面输出信息看一下
console.log("email 查询了")
确实会造成性能问题,所以这个校验还不能放在这里,看有没有更好的办法。

问了一下chatgpt,可以使用防抖来解决这个问题。也可以在提交的时候调用校验,如果有问题,就使用
setError方法来给出提示信息。
这节课学习react hook form的校验模式,默认的校验模式是用户点击submit button之后触发校验。useForm有一个参数mode,通过它可以修改校验模式。

mode有5种值,如下:

注意:使用onChange模式时,会引起多次rerender,从而影响性能。all模式就更不用说了,加上了onBlur和onChange,性能肯定更差。
下面以onBlur模式来试一下效果:
xxxxxxxxxx71const form = useForm<formValues>({2 defaultValues: {3 4 },5 mode: "onBlur"6 }7);onBlur模式其实很好,因为有isValid可以作为submit button的判断依据,所以如果校验没有通过,submit button是可以disabled,避免用户点击。

这节课学习手动触发校验。
useForm提供了trigger方法,可以触发单个、多个input的校验,也可以触发整个form的校验。需要注意的是下面图中的rules说明了:react hook form的隔离渲染优化仅仅适用于手动触发单个input的校验,如果是多个input校验或者整个form校验,则会引整个formState的重新渲染。

xxxxxxxxxx11const { trigger } = useForm();1、手动触发整个form校验
xxxxxxxxxx291import { useForm, useFieldArray, type FieldErrors } from "react-hook-form";2import { DevTool } from '@hookform/devtools'3import { useEffect } from "react";45type formValues = {6 7}89export const YoutubeForm = () => {1011 const form = useForm<formValues>({12 defaultValues: {13 14 },15 mode: "onBlur"16 });17 18 // 从 useForm 中解构出 trigger19 const { register, control, handleSubmit, formState, watch, getValues, setValue, reset, trigger } = form;2021 return (22 <div>23 <form onSubmit={handleSubmit(onSubmit, onError)}>24 {/* 手动触发整个form校验 */}25 <button type="button" onClick={() => trigger()}>Trigger</button>26 </form>27 </div>28 );29};看一下效果:

2、触发单个field校验
xxxxxxxxxx11<button type="button" onClick={() => trigger("email")}>Trigger</button>
3、触发多个field校验
xxxxxxxxxx11<button type="button" onClick={() => trigger(["email", "channel"])}>Trigger</button>
学习的react hook form更深入的内容,就是下面这些:

虽然react hook form本身已经很好了,但是还是有一些优化的地方。比如说在校验的时候,能不能写的更简单一点。
Yup没有自己的官网,可以到它的github里面查看文档:https://github.com/jquense/yup?tab=readme-ov-file。
Yup is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformation.
Yup可以结合react hook form一起使用,先定义schema,然后校验数据。
创建文件mycode\rhf-demo\src\components\YupYoutubeForm.tsx,编写简单的表单。
xxxxxxxxxx561import { useForm } from "react-hook-form"2import { DevTool } from '@hookform/devtools'34type FormValues = {5 username: string;6 email: string;7 channel: string;8}910export const YupYoutubeForm = () => {1112 const { register, handleSubmit, formState, control } = useForm<FormValues>({13 defaultValues: {14 username: "",15 email: "",16 channel: "",17 }18 });1920 const { errors } = formState;2122 const onSubmit = (data: FormValues) => {23 console.log("Form Submit", data);24 }2526 return (27 <div>28 <form onSubmit={handleSubmit(onSubmit)} noValidate>29 <div className="form-control">30 <label htmlFor="username">Username</label>31 <input type="text" id="username" {register("username")} />32 {33 errors.username && <p className="error">{errors.username?.message}</p>34 }35 </div>36 <div className="form-control">37 <label htmlFor="email">E-mail</label>38 <input type="text" id="email" {register("email")} />39 {40 errors.email && <p className="error">{errors.email.message}</p>41 }42 </div>43 <div className="form-control">44 <label htmlFor="channel">Channel</label>45 <input type="text" id="channel" {register("channel")} />46 {47 errors.channel && <p className="error">{errors.channel.message}</p>48 }49 </div>50 <button>Submit</button>51 </form>52 {/* 使用control将表单和DevTool关联起来 */}53 <DevTool control={control} />54 </div>55 )56}
安装依赖npm i yup @hookform/resolvers,这里的@hookform/resolvers是连接react hook form和yup的桥梁。
引入依赖:
xxxxxxxxxx21import { yupResolver } from '@hookform/resolvers/yup'2import * as yup from "yup"定义yup validation schema:
xxxxxxxxxx71const schema = yup.object({2 // 直接使用 string() 来表示类型,required()表示必填,里面的参数是报错信息3 username: yup.string().required("Username is required"),4 // 直接使用 email() ,这是yup的内置类型,表示email的格式。里面的参数是报错信息5 email: yup.string().email("Email format is not valid").required("Email is required"),6 channel: yup.string().required("Channel is required")7})将schema和react hook form连接起来,定义userForm里面的resolver参数:
xxxxxxxxxx81const { register, handleSubmit, formState, control } = useForm<FormValues>({2 defaultValues: {3 username: "",4 email: "",5 channel: "",6 },7 resolver: yupResolver(schema)8 });连接起来之后,react hook form就会按照schema定义的规则进行校验,并生成errors信息。我下面粘贴上完整的代码:
xxxxxxxxxx671import { useForm } from "react-hook-form"2import { DevTool } from '@hookform/devtools'3import { yupResolver } from '@hookform/resolvers/yup'4import * as yup from "yup"56type FormValues = {7 username: string;8 email: string;9 channel: string;10}1112const schema = yup.object({13 // 直接使用 string() 来表示类型,required()表示必填,里面的参数是报错信息14 username: yup.string().required("Username is required"),15 // 直接使用 email() ,这是yup的内置类型,表示email的格式。里面的参数是报错信息16 email: yup.string().email("Email format is not valid").required("Email is required"),17 channel: yup.string().required("Channel is required")18})1920export const YupYoutubeForm = () => {2122 const { register, handleSubmit, formState, control } = useForm<FormValues>({23 defaultValues: {24 username: "",25 email: "",26 channel: "",27 },28 resolver: yupResolver(schema)29 });3031 const { errors } = formState;3233 const onSubmit = (data: FormValues) => {34 console.log("Form Submit", data);35 }3637 return (38 <div>39 <form onSubmit={handleSubmit(onSubmit)} noValidate>40 <div className="form-control">41 <label htmlFor="username">Username</label>42 <input type="text" id="username" {register("username")} />43 {44 errors.username && <p className="error">{errors.username?.message}</p>45 }46 </div>47 <div className="form-control">48 <label htmlFor="email">E-mail</label>49 <input type="text" id="email" {register("email")} />50 {51 errors.email && <p className="error">{errors.email.message}</p>52 }53 </div>54 <div className="form-control">55 <label htmlFor="channel">Channel</label>56 <input type="text" id="channel" {register("channel")} />57 {58 errors.channel && <p className="error">{errors.channel.message}</p>59 }60 </div>61 <button>Submit</button>62 </form>63 {/* 使用control将表单和DevTool关联起来 */}64 <DevTool control={control} />65 </div>66 )67}查看效果:

感觉代码会简单很多。
zod与yup类似,都是用于和react hook form结合使用,用来校验表单的。
Zod is a TypeScript-first validation library. Using Zod, you can define schemas you can use to validate data, from a simple string to a complex nested object.
创建mycode\rhf-demo\src\components\ZodYoutubeForm.tsx,编写简单表单。
xxxxxxxxxx571import { useForm } from "react-hook-form"2import { DevTool } from '@hookform/devtools'34type FormValues = {5 username: string;6 email: string;7 channel: string;8}910export const ZodYoutubeForm = () => {1112 const { register, handleSubmit, formState, control } = useForm<FormValues>({13 defaultValues: {14 username: "",15 email: "",16 channel: "",17 },18 });1920 const { errors } = formState;2122 const onSubmit = (data: FormValues) => {23 console.log("Form Submit", data);24 }2526 return (27 <div>28 <form onSubmit={handleSubmit(onSubmit)} noValidate>29 <div className="form-control">30 <label htmlFor="username">Username</label>31 <input type="text" id="username" {register("username")} />32 {33 errors.username && <p className="error">{errors.username?.message}</p>34 }35 </div>36 <div className="form-control">37 <label htmlFor="email">E-mail</label>38 <input type="text" id="email" {register("email")} />39 {40 errors.email && <p className="error">{errors.email.message}</p>41 }42 </div>43 <div className="form-control">44 <label htmlFor="channel">Channel</label>45 <input type="text" id="channel" {register("channel")} />46 {47 errors.channel && <p className="error">{errors.channel.message}</p>48 }49 </div>5051 <button>Submit</button>52 </form>53 {/* 使用control将表单和DevTool关联起来 */}54 <DevTool control={control} />55 </div>56 )57}安装依赖npm i zod @hookform/resolvers。
引入依赖:
xxxxxxxxxx21import { zodResolver } from '@hookform/resolvers/zod'2import * as z from "zod";定义schema:
xxxxxxxxxx61const schema = z.object({2 username: z.string().nonempty("Username is required"),3 // 这里老师是这样写的 z.string().nonempty().email(),但是在zod4中 z.string().email() 的用法已经被弃用。要直接使用 z.email() 这种方法才行。4 email: z.email("Email format is not valid").nonempty("Email is required"),5 channel: z.string().nonempty("Channel is required")6})将schema和react hook form结合起来:
xxxxxxxxxx81const { register, handleSubmit, formState, control } = useForm<FormValues>({2 defaultValues: {3 username: "",4 email: "",5 channel: "",6 },7 resolver: zodResolver(schema)8 });完整代码:
xxxxxxxxxx671import { useForm } from "react-hook-form"2import { DevTool } from '@hookform/devtools'3import { zodResolver } from '@hookform/resolvers/zod'4import * as z from "zod";56const schema = z.object({7 username: z.string().nonempty("Username is required"),8 // 这里老师是这样写的 z.string().nonempty().email(),但是在zod4中 z.string().email() 的用法已经被弃用。要直接使用 z.email() 这种方法才行。9 email: z.email("Email format is not valid").nonempty("Email is required"),10 channel: z.string().nonempty("Channel is required")11})1213type FormValues = {14 username: string;15 email: string;16 channel: string;17}1819export const ZodYoutubeForm = () => {2021 const { register, handleSubmit, formState, control } = useForm<FormValues>({22 defaultValues: {23 username: "",24 email: "",25 channel: "",26 },27 resolver: zodResolver(schema)28 });2930 const { errors } = formState;3132 const onSubmit = (data: FormValues) => {33 console.log("Form Submit", data);34 }3536 return (37 <div>38 <form onSubmit={handleSubmit(onSubmit)} noValidate>39 <div className="form-control">40 <label htmlFor="username">Username</label>41 <input type="text" id="username" {register("username")} />42 {43 errors.username && <p className="error">{errors.username?.message}</p>44 }45 </div>46 <div className="form-control">47 <label htmlFor="email">E-mail</label>48 <input type="text" id="email" {register("email")} />49 {50 errors.email && <p className="error">{errors.email.message}</p>51 }52 </div>53 <div className="form-control">54 <label htmlFor="channel">Channel</label>55 <input type="text" id="channel" {register("channel")} />56 {57 errors.channel && <p className="error">{errors.channel.message}</p>58 }59 </div>6061 <button>Submit</button>62 </form>63 {/* 使用control将表单和DevTool关联起来 */}64 <DevTool control={control} />65 </div>66 )67}查看效果:

之前看zod视频、文档,之所以看不懂,是因为我看到明明已经定义了数据的typescript类型,为什么在schema里面还要定义一遍?
学习了之后才发现,zod在react hook form中是专门用来校验的,这一点理解清楚了就好做了。
这节课学习怎么将mui结合react hook form一起使用。使用mui components创建一个login form。
安装依赖npm install @mui/material @emotion/react @emotion/styled。
创建mycode\rhf-demo\src\components\MuiLoginForm.tsx,编写简单的表单。
xxxxxxxxxx181import { TextField, Button, Stack } from '@mui/material'23export default function MuiLoginForm() {4 return (5 <>6 <h1>Login</h1>7 <form noValidate>8 <Stack spacing={2} width={400}>9 <TextField label="Email" type="email" />10 <TextField label="Password" type="password" />11 <Button type="submit" variant="contained" color="primary">12 Login13 </Button>14 </Stack>15 </form>16 </>17 )18}结合react hook form进行编写:
xxxxxxxxxx441import { DevTool } from '@hookform/devtools';2import { TextField, Button, Stack } from '@mui/material'3import { useForm } from 'react-hook-form'45type formValues = {6 email: string;7 password: string;8}910export default function MuiLoginForm() {1112 const { register, handleSubmit, formState, control } = useForm<formValues>({13 defaultValues: {14 email: "",15 password: ""16 }17 });1819 const { errors } = formState;2021 const onSubmit = (data: formValues) => {22 console.log(data);23 }2425 return (26 <>27 <h1>Login</h1>28 <form onSubmit={handleSubmit(onSubmit)} noValidate>29 <Stack spacing={2} width={400}>30 <TextField label="Email" type="email" {register("email", {31 required: "Email is required"32 })} error={!!errors.email} helperText={errors.email?.message} />33 <TextField label="Password" type="password" {register("password", {34 required: "Password is required"35 })} error={!!errors.password} helperText={errors.password?.message} />36 <Button type="submit" variant="contained" color="primary">37 Login38 </Button>39 </Stack>40 </form>41 <DevTool control={control} />42 </>43 )44}需要注意的就是errors的展示方法,mui的组件内置了属性,可以直接写在上面。
注释掉App.tsx和main.tsx里面引入的css文件,查看效果:

老师没有讲解Controller,但是我看到很多人使用react hook form的时候,都用到了Controller,难道真的需要学习这个吗?
应该是需要的,因为react hook form提倡使用非受控组件和原生HTML表单输入,如果使用的是第三方UI库提供的组件,那么这些组件一般都是受控组件,此时就需要使用Controller来更好的控制这些UI组件。
问了一下chatgpt。
在 React Hook Form (RHF) 中,
Controller是一个非常核心的组件,用来在 React Hook Form 和第三方 UI 组件(例如 MUI、Ant Design、React-Select 等)之间建立连接。🧩 一句话解释
Controller是一个“中间层”,让那些不是原生表单元素(如<input>、<select>)的自定义组件也能受 React Hook Form 控制。🔍 为什么需要
ControllerReact Hook Form 对原生的表单元素(
input,select,textarea)支持很好,可以直接用register()来注册:xxxxxxxxxx11<input {register("username")} />但是像这样第三方 UI 组件就不行了 👇
xxxxxxxxxx31<Autocomplete ... /> // 来自 MUI2<Select ... /> // 来自 react-select3<DatePicker ... /> // 来自 antd这些组件不一定触发标准的
onChange、onBlur事件,也不一定用value属性来表示当前值。 所以你不能简单地用register()。这时就需要
Controller来手动指定:
- 值(
value)应该传给哪个属性;- 改变事件(
onChange)应该怎么触发;- 验证逻辑由 RHF 管理。
⚙️ 基本用法
xxxxxxxxxx301import { useForm, Controller } from "react-hook-form";2import { TextField } from "@mui/material";34function MyForm() {5const { control, handleSubmit } = useForm({6defaultValues: { username: "" },7});89const onSubmit = (data) => console.log(data);1011return (12<form onSubmit={handleSubmit(onSubmit)}>13<Controller14name="username"15control={control}16rules={{ required: "Username is required" }}17render={({ field, fieldState }) => (18<TextField19{field} // 提供 value, onChange, onBlur 等20label="Username"21error={!!fieldState.error}22helperText={fieldState.error?.message}23/>24)}25/>2627<button type="submit">Submit</button>28</form>29);30}🧠
Controller的核心属性
属性 作用 name字段名(必须)。用我的理解就是定义的变量名 control来自 useForm()的控制对象rules验证规则(可选) defaultValue默认值(建议在 useForm里统一设置)render一个 render prop,用来渲染表单组件
render回调会收到一个对象,常用属性如下:xxxxxxxxxx111{2field: {3onChange, onBlur, value, name, ref4},5fieldState: {6invalid, isTouched, error7},8formState: {9isSubmitting, isValid10}11}🧩 举例:配合
react-selectxxxxxxxxxx111<Controller2name="country"3control={control}4render={({ field }) => (5<Select6{field}7options={[{ value: 'us', label: 'USA' }, { value: 'cn', label: 'China' }]}8onChange={(val) => field.onChange(val.value)} // 手动定义值变化9/>10)}11/>🧭 总结
情况 用法 原生 <input>/<select>直接用 register()第三方 / 自定义组件 用 Controller包起来想统一控制验证、值和状态 Controller是首选
创建一个mycode\rhf-demo\src\components\ControllerForm.tsx,编写简单的组件:
xxxxxxxxxx701import { useForm, Controller } from 'react-hook-form'2import { z } from 'zod'3import { zodResolver } from '@hookform/resolvers/zod'4import { TextField, Button, MenuItem, Stack } from '@mui/material'56// 定义zod schema7const schema = z.object({8 name: z.string().min(2, "名字至少2个字符"),9 email: z.email("请输入合法的邮箱地址"),10 // coerce的本意是胁迫,这里的意思是将input的值强制转为合适的类型11 age: z.coerce.number("请输入数字").min(18, "必须年满18岁"),12 gender: z.enum(["male", "female"], "请选择性别")13})1415// 推导表单类型16type FormData = z.infer<typeof schema>;1718export default function ControllerForm() {19 // 初始化useForm20 const {21 control,22 handleSubmit,23 formState: { errors, isSubmitting }24 } = useForm<FormData>({25 defaultValues: {26 name: "",27 email: "",28 age: 18,29 gender: "male"30 },31 resolver: zodResolver(schema)32 });3334 const onSubmit = (data: FormData) => {35 console.log("form submit", data);36 }3738 return (39 <>40 <form onSubmit={handleSubmit(onSubmit)}>4142 <Stack spacing={2} width={400}>43 <Controller name="name" control={control} render={({ field }) => (44 <TextField {field} label="姓名" variant="outlined" error={!!errors.name} helperText={errors.name?.message} />45 )} />4647 <Controller name="email" control={control} render={({ field }) => (48 <TextField {field} label="邮箱" variant="outlined" error={!!errors.email} helperText={errors.email?.message} />49 )} />5051 <Controller name="age" control={control} render={({ field }) => (52 <TextField {field} type="number" label="年龄" variant="outlined" error={!!errors.age} helperText={errors.age?.message} />53 )} />5455 <Controller name="gender" control={control} render={({ field }) => (56 <TextField {field} select label="性别" variant="outlined" error={!!errors.gender} helperText={errors.gender?.message}>57 <MenuItem value="male">男</MenuItem>58 <MenuItem value="female">女</MenuItem>59 </TextField>60 )} />6162 <Button variant="contained" color="primary" type="submit" disabled={isSubmitting}>63 {isSubmitting ? "提交中..." : "提交"}64 </Button>65 </Stack>6667 </form>68 </>69 )70}查看效果:

这个案例里面有一个报错,记录一下。当使用
coerce将input值强制转换了,但是使用z.infer推导类型的时候,会将类型推导为unknown。所以会报这种错误。
官网里面有解答,如下:
https://zod.dev/api?id=coercion
那么代码改成这样就行了: