

创建一个简单的form表单,内容如下:

使用vite来创建项目,步骤如下:
如果使用create-react-app命令来创建项目,步骤如下:

创建文件src/components/YoutubeForm.tsx,里面就是一个简单的表单:
xxxxxxxxxx201// src/components/YoutubeForm.tsx23export const YoutubeForm = () => {4 return (5 <div>6 <form>7 <label htmlFor="username"> Username </label>8 <input type="text" id="username" name="username" />910 <label htmlFor="username"> Email </label>11 <input type="text" id="username" name="username" />1213 <label htmlFor="username"> Channel </label>14 <input type="text" id="username" name="username" />1516 <button>Submit </button>17 </form>18 </div>19 );20};修改App.css和index.css里面的样式,直接copy老师的文件即可。然后在App.tsx里面使用表单:
xxxxxxxxxx151// App.tsx23import './App.css'4import { YoutubeForm } from './components/YoutubeForm'56function App() {78 return (9 <>10 <YoutubeForm />11 </>12 )13}1415export default App效果:

安装依赖npm i react-hook-form,在YoutubeForm里面使用。
xxxxxxxxxx231import { useForm } from "react-hook-form";23export const YoutubeForm = () => {45 const form = useForm();67 return (8 <div>9 <form>10 <label htmlFor="username"> Username </label>11 <input type="text" id="username" name="username" />1213 <label htmlFor="username"> Email </label>14 <input type="text" id="username" name="username" />1516 <label htmlFor="username"> Channel </label>17 <input type="text" id="username" name="username" />1819 <button>Submit </button>20 </form>21 </div>22 );23};useForm是一个hook函数,所以按照函数的方式来使用它,它会返回一个对象。
这节课就学习react-hook-form最基本的使用方法。
什么是form state呢?就是form里面可以变化的部分:

在代码中,我们可以这样表示form state:

但是这些对象都要追踪的话,太复杂了,所以需要使用react hook form来帮助我们解决。
useForm()返回的对象form里面,包含一个register方法,register 方法的作用是将表单的输入元素(如 <input>、<select>、<textarea> 等)与 React Hook Form 的表单状态管理绑定起来,从而实现表单数据的收集、验证和提交。
使用register的方法如下:
xxxxxxxxxx271import { useForm } from "react-hook-form";23export const YoutubeForm = () => {45 const form = useForm();6 const { register } = form;7 // 注册username,这样就可以跟踪username输入框8 const { name, ref, onChange, onBlur } = register("username");910 return (11 <div>12 <form>13 <label htmlFor="username"> Username </label>14 {/* 将register返回的内容全部放到input上 */}15 <input type="text" id="username" name={name} ref={ref} onChange={onChange} onBlur={onBlur} />1617 <label htmlFor="email"> Email </label>18 <input type="text" id="email" name="username" />1920 <label htmlFor="channel"> Channel </label>21 <input type="text" id="channel" name="username" />2223 <button>Submit </button>24 </form>25 </div>26 );27};这种使用方法是react hook form帮助我们真正实现的代码,但是在使用的时候,可以使用spread operator来传递参数:
xxxxxxxxxx241import { useForm } from "react-hook-form";23export const YoutubeForm = () => {45 const form = useForm();6 const { register } = form;78 return (9 <div>10 <form>11 <label htmlFor="username"> Username </label>12 <input type="text" id="username" {register("username")} />1314 <label htmlFor="email"> Email </label>15 <input type="text" id="email" {register("email")} />1617 <label htmlFor="channel"> Channel </label>18 <input type="text" id="channel" {register("channel")} />1920 <button>Submit </button>21 </form>22 </div>23 );24};那么,现在react hook form就接管了form state,但是我怎么知道接管了呢?下节课会介绍。
这节课我们来学习react hook form是否真的在manage form state。项目里面安装依赖npm i -D @hookform/devtools,在YoutubeForm里面使用这个工具。
xxxxxxxxxx271import { useForm } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34export const YoutubeForm = () => {56 const form = useForm();7 const { register, control } = form;89 return (10 <div>11 <form>12 <label htmlFor="username"> Username </label>13 <input type="text" id="username" {register("username")} />1415 <label htmlFor="email"> Email </label>16 <input type="text" id="email" {register("email")} />1718 <label htmlFor="channel"> Channel </label>19 <input type="text" id="channel" {register("channel")} />2021 <button>Submit </button>22 </form>23 {/* 使用control将表单和DevTool关联起来 */}24 <DevTool control={control} />25 </div>26 );27};然后在页面右上角可以看到一个小工具:

点击可以看到:

value表示当前值,touched表示是否交互过,dirty表示字段是否已更改。
这节课是说明react hook form管理form state,不会造成页面的rerender,这一点很棒。react hook form遵循非受控组件的输入行为。
为了说明react hook form在管理form state的时候不会造成页面的rerender,那么可以定义一个数字变量,看form交互的时候,数字是否会变化。
xxxxxxxxxx321import { useForm } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34let renderCount = 0;56export const YoutubeForm = () => {78 const form = useForm();9 const { register, control } = form;1011 renderCount++12 return (13 <div>14 {/* 这里为什么要展示renderCount / 2,因为在react项目中使用了StrictMode,组件在dev环境会重新渲染一次,数字就会变为2,如果不知道StrictMode的话,会引起误会 */}15 <h1>Youtube Form {renderCount / 2}</h1>16 <form>17 <label htmlFor="username"> Username </label>18 <input type="text" id="username" {register("username")} />1920 <label htmlFor="email"> Email </label>21 <input type="text" id="email" {register("email")} />2223 <label htmlFor="channel"> Channel </label>24 <input type="text" id="channel" {register("channel")} />2526 <button>Submit </button>27 </form>28 {/* 使用control将表单和DevTool关联起来 */}29 <DevTool control={control} />30 </div>31 );32};可以看到,在表单交互的时候,数字并没有变化,这就说明了react hook form管理form state,不会造成页面重新渲染。

为什么react hook form能够做到监听值的变化,而不引起页面的重新渲染?
🧠 背景:普通表单为什么会频繁 re-render?
在传统 React 表单中,你可能会这样写:
xxxxxxxxxx31const [username, setUsername] = useState('');23<input value={username} onChange={(e) => setUsername(e.target.value)} />每次输入一个字母,
setUsername都会触发组件的 重新渲染。 如果表单有很多字段,这种模式就会造成大量 无意义的 re-render。⚙️ React Hook Form 的不同之处
React Hook Form 的核心设计目标是:
让表单状态的变更不强制触发 React 组件的重新渲染。
它是通过下面这三个机制实现的:
① 使用独立的 “subscription” 模型(发布-订阅)
- RHF 使用一个 内部的 store(类似全局表单状态容器) 来保存所有字段的值与验证状态。
- 每个字段只订阅自己关心的部分(例如:自己的值、错误状态)。
- 当某个字段值改变时,只通知订阅它的监听器,而不是整个表单组件。
📦 类似这样:
xxxxxxxxxx31Form Store2├── username -> only <Input name="username" /> re-renders3├── email -> only <Input name="email" /> re-renders因此,不会造成整页刷新或整个表单 re-render。
②
register()不会把字段变成受控组件在普通 React 中:
xxxxxxxxxx11<input value={value} onChange={} />这种模式叫“受控组件”,会导致每次输入都触发渲染。
而 RHF 的:
xxxxxxxxxx11<input {register("username")} />其实是非受控组件(uncontrolled)。 RHF 通过 ref 直接读取 DOM 的值,而不是通过 React 的 state。
👉 也就是说,输入框的值存在 DOM 里,而不是存在 React state 里。 所以 React 不需要重新渲染来同步输入。
③
watch()和useWatch()提供选择性订阅RHF 提供两个监听值变化的方式:
watch():直接在组件中取值,但会在父组件 render 时运行;useWatch():更细粒度的订阅,只重新渲染依赖的部分。例如:
xxxxxxxxxx21const { control } = useForm();2const username = useWatch({ control, name: "username" });这个组件只会在
username变化时更新,而不是整个表单。🧩 小总结
机制 说明 好处 独立 store + subscription 字段独立订阅自己的变化 避免全局 re-render 非受控组件(uncontrolled input) 直接通过 DOM ref 管理值 不依赖 React state useWatch 精准订阅特定字段 局部更新 ⚡ 总结一句话
React Hook Form 用 “非受控组件 + 订阅模型” 替代 “受控 state 模型”,从而避免不必要的 React re-render。
需要注意的是,设置了校验规则之后,如果不断触发input的校验报错,那么会引起页面的rerender。为什么会这样呢?
非常棒的问题!👏 你观察得很细,也确实抓到了 React Hook Form(RHF)性能机制的一个关键细节。
这个现象其实 不矛盾,而是 RHF 有意为之 的一部分设计。我们来细讲。
🎯 结论先说:
RHF 的“无重渲染”特性只在 字段值变化(value change) 时生效。 一旦涉及到 校验状态(error state)变化,它会 触发必要的 re-render,但只更新受影响的组件,而非整个表单。
🧩 为什么?
让我们回忆一下 RHF 的内部状态分层:
层级 存的是什么 是否引发重新渲染 field value 字段的实际输入值 ❌ 不引发(非受控) field state touched, dirty, error 等状态 ✅ 可能引发(受控) form state 全局状态,如 isValid,isSubmitting✅ 可能引发(订阅更新) 当你只输入内容时(没校验或没报错):
- RHF 只是更新内部的 store;
- input 是非受控的;
- 所以不会 re-render。
但当你设置了校验规则,例如:
xxxxxxxxxx121<Controller2name="email"3control={control}4rules={{ required: "Email required", pattern: /@/ }}5render={({ field, fieldState }) => (6<TextField7{field}8error={!!fieldState.error}9helperText={fieldState.error?.message}10/>11)}12/>每当输入内容使得校验状态变化(比如从无错误 → 有错误 / 错误内容不同):
- RHF 内部的
fieldState.error变了;- 订阅了该
fieldState的组件(也就是上面 render 的<TextField>)被通知更新;- 因此,这个 input 重新渲染,但其他字段不会。
⚙️ 总结成一句话:
✅ 值变化 → 只更新 store,不 re-render ⚠️ 校验状态变化 → 更新订阅的字段视图(必要的 re-render)
这不矛盾,而是 React Hook Form 的精细化优化策略。
🧠 额外技巧:减少验证引发的 re-render
如果你觉得验证导致的重渲染太频繁,可以做两件事:
调整验证时机
xxxxxxxxxx11const { control } = useForm({ mode: "onBlur" });👉 只在失焦时校验,不在每次输入时触发。
延迟验证(自定义触发) 用
trigger("fieldName")手动触发验证,而不是在输入中实时执行。
我好像有点害怕页面的rerender,因为我写vue的缘故,当页面rerender之后,里面使用
v-model绑定的值会丢失,那么vue里面有rerender吗?我从来没有注意过这个问题。问:vue里面有类似react里面的rerender吗?
答:
简而言之:
Vue 中没有直接对应 React 中“组件
re-render(重新渲染)”的概念和机制。
1. React 的机制:Re-render (重新渲染)
在 React 中,组件的渲染流程是基于组件函数重新执行的:
- 触发:当组件的
State或Props发生变化时。- 过程:整个组件函数会重新执行,生成一个新的虚拟 DOM (Virtual DOM) 树。
- 对比:React 将新的 V-DOM 树与旧的 V-DOM 树进行 Diff(对比/调和/Reconciliation)。
- 更新:只有发现差异的部分,React 才会去更新真实的 DOM。
这种机制的特点是:默认情况下,组件内部的代码(包括 JSX)会完整地重新执行。为了优化性能,开发者需要手动使用
memo、useCallback、useMemo或shouldComponentUpdate来阻止不必要的重新渲染。2. Vue 的机制:细粒度响应式 (Fine-Grained Reactivity)
在 Vue 中,组件的更新流程是基于响应式数据依赖追踪的:
- 创建 (Tracking):在组件首次渲染时,Vue 的响应式系统会“监听”你在模板中使用了哪些
ref或reactive数据。这个过程被称为依赖收集。- 触发 (Triggering):只有当你修改了被模板使用的那个特定的响应式数据时,Vue 的响应式系统才会收到通知。
- 更新:Vue 会找到唯一需要更新的那个 Reactive Effect(通常是一个 Watcher 或一个渲染函数)。它只会重新运行依赖于这个变化数据的代码块,并生成局部的 V-DOM 树。
- 局部修补 (Patch):Vue 的编译器和运行时优化使其能够非常高效地只修补 (Patch) 真实 DOM 中发生变化的那一小部分。
这种机制的特点是:Vue 的更新是“自动且精准”的。你不需要担心整个组件函数会重新执行,也不需要手动阻止不必要的重新渲染。
总结对比
特性 React (re-render) Vue (响应式更新) 触发机制 State或Props变化被使用的响应式数据变化 执行范围 整个组件函数重新执行,生成完整 V-DOM 只执行依赖于变化数据的代码块,生成局部 V-DOM 优化方式 开发者手动使用 memo、useMemo、shouldComponentUpdate来阻止重新渲染。框架自动优化,更新的粒度非常细,通常不需要手动干预。 性能理念 默认为不精确(宽泛),开发者手动优化。 默认为精确(细粒度),框架自动追踪。 因此,当 Vue 中的响应式数据发生变化时,它对应的行为是“自动和最小化的更新”或“重新运行其依赖的副作用函数(Effect)”,而不是 React 那种广义上的“组件重新渲染”。
其实我理解的rerender和实际上的rerender不是一回事,我理解的rerender很可能是
page reload这种东西,那么react rerender到底是什么呢?很多人用 React 一段时间后发现“页面卡”、“动一下全刷新”,根本原因通常都在于对 rerender(重新渲染)机制 理解不深。 我们来系统地讲一遍 👇
🧠 一、先说什么是“rerender”
在 React 中,当组件的 state 或 props 发生变化 时,React 会触发组件函数再次执行,这就是一次 rerender(重新渲染)。
⚠️ 注意:
“rerender” ≠ “DOM 更新”。 React 的重新渲染是 虚拟 DOM(Virtual DOM)层面 的重新计算,只有当新旧虚拟 DOM 不同时,才会导致真实 DOM 更新。
⚙️ 二、什么会引发 rerender?
触发原因 示例 说明 useState更新setCount(count + 1)本组件 rerender 父组件 rerender 父组件执行,子组件也 rerender(除非被 memo) useReducerdispatchdispatch 改变 state,会 rerender useContext的值改变所有消费该 context 的组件都会 rerender props 引用变化 即使内容相同,只要引用不同也会 rerender(对象/函数) 💥 三、rerender 的影响(你该警惕的地方)
1️⃣ 性能影响:多次 render 会浪费计算资源
React 每次 rerender 都会重新执行组件函数:
- 重新计算 JSX;
- 重新绑定事件;
- 重新计算 Hook 调用;
- 如果有昂贵逻辑(如数据处理、复杂渲染),性能会迅速下降。
👉 解决办法:
- 用
useMemo缓存计算结果;- 用
useCallback缓存函数;- 用
React.memo()包裹组件。2️⃣ 问题:不必要的 rerender
父组件更新时,所有子组件默认都会重新渲染,即使 props 没变。
例子:
xxxxxxxxxx141function Parent() {2const [count, setCount] = useState(0);3return (4<>5<button onClick={() => setCount(c => c + 1)}>+</button>6<Child />7</>8);9}1011const Child = () => {12console.log("Child render"); // ⚠️ 每次 Parent 更新都会打印13return <div>Child</div>;14};✅ 解决方案:
xxxxxxxxxx41const Child = React.memo(() => {2console.log("Child render"); // 只渲染一次3return <div>Child</div>;4});3️⃣ 对异步副作用的影响
如果你的组件里有异步逻辑,例如:
xxxxxxxxxx31useEffect(() => {2fetchData(); // 假设是个异步请求3}, []);一旦 rerender 逻辑不当,容易导致:
- 重复请求;
- 闭包陷阱(拿到旧 state);
- 状态被覆盖。
⚠️ 所以要确保:
useEffect的依赖正确;- 使用
useCallback稳定函数引用;- 在异步中用
isMounted或 abortController 控制请求。4️⃣ 对 React Hook Form、React Query、Redux 等库的影响
像 React Hook Form 之所以性能好,就是因为它避免了无意义的 rerender。
- 如果你在表单里频繁触发
setValue/setError;- 或者 React Query 每次请求完都让整个页面刷新; 👉 性能就会下降。
解决方法是“订阅局部变化”:
- RHF →
Controller+useWatch;- React Query → 拆分 Query、
select;- Redux → 使用
useSelector时拆分 state。⚡ 四、优化 rerender 的核心思路
场景 优化手段 原理 父子组件频繁刷新 React.memo跳过 props 未变化的重新渲染 函数作为 props useCallback保持函数引用稳定 大计算量渲染 useMemo缓存计算结果 Context 改变太频繁 拆分 Context / 使用 selector 减少订阅范围 表单状态频繁变 非受控组件 / useWatch 精细订阅 🎯 五、最重要的理解:Rerender 不是坏事
React 的 rerender 并不一定慢,它只是重新执行函数。 真正影响性能的,是“无意义的 rerender”。
也就是说:
- 小组件 rerender 没问题;
- 关键是 不要让它们级联触发(parent → child → grandchild...)。
✅ 总结一句话
React rerender 的本质是组件函数重新执行。 合理控制 rerender,可以极大提升性能与可维护性。
可以从上面看到,react的rerender机制还是非常复杂的,所以产生了nextjs这样的框架,那么nextjs框架对于react的rerender有什么优化吗?为什么我在学习nextjs的时候,没有怎么使用
useMemo、useCallback这些函数呢?非常好的问题,这说明你已经从“怎么用 React”上升到了“React 框架是怎么优化性能的”这个层面。👏
其实你观察得非常准确 —— 在 Next.js(尤其是 v13+ 的 App Router 架构) 中, 你确实会发现: 👉 很多地方根本不需要手动用
useMemo、useCallback来优化渲染性能。这并不是因为 Next.js 不会发生 rerender,而是因为 Next.js 把大部分渲染优化提前到了“服务器端”。 我们来分层分析一下为什么。👇
🧩 一、React 自身 rerender 的痛点
在纯 React(前端渲染)项目中:
- 所有状态、组件、数据请求都在 浏览器端运行;
- 页面渲染逻辑全靠客户端 React 控制;
- 因此频繁的
setState、useEffect、函数重建、props 传递都会导致 rerender。你就需要用:
React.memo避免子组件重复渲染;useCallback缓存函数;useMemo缓存复杂计算。⚙️ 二、Next.js 的重大不同点:默认是服务端渲染(Server Components)
从 Next.js 13 (App Router) 开始,引入了 React 官方的 Server Components 模式。 这彻底改变了渲染逻辑 👇
对比 传统 React Next.js App Router 组件执行位置 浏览器端 默认在服务器端 状态管理 前端状态 服务端计算后传结果 网络请求 浏览器 fetch 服务器直接 fetch(更快) Rerender 范围 前端全量渲染 仅 Client Components rerender 优化需求 必须用 useMemo/useCallback 大部分情况下不需要 🧠 三、深入理解:Server Components 避免了很多“rerender 场景”
举个例子👇
xxxxxxxxxx51// app/page.tsx2export default async function Page() {3const data = await getData(); // 服务端执行4return <Main data={data} />;5}这里的
Page是 Server Component:
- 它只在服务端执行一次;
- React 直接把渲染好的 HTML 发给浏览器;
- 浏览器拿到后只负责展示;
- 不存在 “state 改变 → rerender” 这个过程。
💡 所以你根本不需要
useMemo、useCallback—— 组件根本不会在客户端重新执行。🧭 四、那 Next.js 里什么时候会重新渲染?
只有这两种情况:
使用 “Client Component” 时
xxxxxxxxxx61"use client";23export default function Counter() {4const [count, setCount] = useState(0);5return <button onClick={() => setCount(c => c + 1)}>{count}</button>;6}🔸 这个组件才是传统的 React 客户端逻辑。 在这种情况下,
useMemo、useCallback才有意义。当 props 变化时
- Next.js 的 Server Components 会智能 diff;
- 只更新需要重新渲染的部分;
- 所以 rerender 的范围非常小,性能损耗远小于纯 React SPA。
🧠 五、Next.js 的其他隐形优化手段
Next.js 在底层做了很多性能优化,让你不必手写优化:
优化点 说明 🔹 Server Components 避免前端 rerender,直接在服务器端计算 🔹 Streaming + Partial Rendering 页面分块流式渲染,减少阻塞 🔹 缓存 (Route Cache / Data Cache) 自动缓存数据请求和组件树 🔹 React Suspense + Lazy Loading 自动分块渲染组件,减少初始负担 🔹 Selective Hydration 只激活需要交互的组件部分 🔹 静态优化 (Static Rendering) 无需请求的页面直接预生成 HTML 👉 因此,在 Next.js 里你几乎感觉不到 “性能调优的负担”。
🧩 六、简单理解一句话:
在传统 React 里,组件是渲染的最小单位。 在 Next.js(App Router)里,Server Component 树是渲染的最小单位。
也就是说,Next.js 在“更高一层”就把 rerender 控制住了。 你写的组件大多只渲染一次,因此根本不需要频繁用
useMemo/useCallback。✅ 七、总结对比表
特性 纯 React SPA Next.js (App Router) 渲染位置 浏览器 服务端(默认) rerender 频率 高频 低频 性能瓶颈 React diff + 组件重建 网络/SSR 流式传输 优化手段 useMemo / useCallback / memo 默认启用缓存 + Streaming 开发体验 手动优化 自动优化 💬 简短总结一句话:
你在 Next.js 里很少用
useMemo、useCallback, 并不是它们没用了,而是 Next.js 通过 Server Components + Cache + Streaming,把大部分 rerender 问题在“框架层”就解决掉了。
这节课学习form提交,有三步来完成。
1、定义一个提交函数,里面写提交的逻辑
xxxxxxxxxx31const onSubmit = () => {2 console.log("Form Submit");3}2、从useForm()的返回结果,解构出handleSubmit方法
xxxxxxxxxx11const { register, control, handleSubmit } = form;3、在<form>上绑定onSubmit事件,使用onSubmit={handleSubmit(onSubmit)},这样做了之后,在onSubmit函数上,可以接收到form里面的数据。之后就可以利用这个数据进行逻辑操作。注意react hook form要求typescript类型,所以使用useForm时要定义ts类型,接收的data参数也要定义类型。
xxxxxxxxxx431import { useForm } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34let renderCount = 0;56type formValues = {7 username: string8 email: string9 channel: string10}1112export const YoutubeForm = () => {1314 const form = useForm<formValues>();15 const { register, control, handleSubmit } = form;1617 // 提交操作的逻辑18 const onSubmit = (data: formValues) => {19 console.log("Form Submit", data);20 }2122 renderCount++23 return (24 <div>25 <h1>Youtube Form {renderCount / 2}</h1>26 {/* 使用onSubmit方法 */}27 <form onSubmit={handleSubmit(onSubmit)}>28 <label htmlFor="username"> Username </label>29 <input type="text" id="username" {register("username")} />3031 <label htmlFor="email"> Email </label>32 <input type="text" id="email" {register("email")} />3334 <label htmlFor="channel"> Channel </label>35 <input type="text" id="channel" {register("channel")} />3637 <button>Submit </button>38 </form>39 {/* 使用control将表单和DevTool关联起来 */}40 <DevTool control={control} />41 </div>42 );43};效果:

之前我们已经学习了管理form state,提交form data,接下来我们学习表单校验和校验结果反馈。

react hook form支持各种HTML校验规则,比如说:必填、最小程度和最大长度、最小值和最大值、设置自定义校验模式。

这节课学习最简单的required规则。在form标签上需要使用noValidate属性,这个属性将阻止浏览器的校验,让react hook form的校验接管。
校验规则写在register的第二个参数,是一个对象,key:value,key表示校验规则,value表示校验出错误时显示的文字信息。
默认情况下,validations只在表单提交的时候触发,当然后面会讨论到其他情况。先看最简单的必填校验。
xxxxxxxxxx451import { useForm } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34let renderCount = 0;56type formValues = {7 username: string8 email: string9 channel: string10}1112export const YoutubeForm = () => {1314 const form = useForm<formValues>();15 const { register, control, handleSubmit } = form;1617 const onSubmit = (data: formValues) => {18 console.log("Form Submit", data);19 }2021 renderCount++22 return (23 <div>24 <h1>Youtube Form {renderCount / 2}</h1>25 <form onSubmit={handleSubmit(onSubmit)}>26 <label htmlFor="username"> Username </label>27 {/* 在register的第二个参数中,写校验规则 */}28 <input type="text" id="username" {register("username", {29 required: "Username is required"30 })} />3132 <label htmlFor="email"> Email </label>33 <input type="text" id="email" {register("email")} />3435 <label htmlFor="channel"> Channel </label>36 <input type="text" id="channel" {register("channel", {37 required: "Channel is required"38 })} />3940 <button>Submit </button>41 </form>42 <DevTool control={control} />43 </div>44 );45};效果:

可以看到,在devtool里面,出现了校验相关的信息。不过这种提示信息不是我所熟悉的在输入框下面出现的红色字体提示信息,说明我要使用返回的Error Type和MESSAGE来展示,这部分后面会讲到。
下面讲一下pattern校验方式怎么做。register的第二个参数对象,key就是pattern,value是一个对象,里面有两个参数,value表示正则规则,message表示校验错误时的提示信息。
xxxxxxxxxx61<input type="text" id="email" {register("email", {2 pattern: {3 value: /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,4 message: "Invalid email format"5 }6 })} />效果:

实际上required规则也可以写成对象的形式:
xxxxxxxxxx61<input type="text" id="channel" {register("channel", {2 required: {3 value: true,4 message: "Channel is required"5 }6 })} />需要注意的是,当submit提交触发了校验之后,在有错误的field里面再次输入,会触发校验,此时不需要submit来触发校验,这一点要注意。
上节课学习了怎么设置校验规则,这节课来讲解怎么展示校验规则。
从useForm()中解构出formState,这个参数包含很多信息,这里只使用它的errors来展示错误信息。
修改了一些样式,如下:

展示错误信息很简单,因为有了react hook form的帮助,只需要在有校验规则的form-item下面直接展示errors相关信息即可,<p className="error">{errors.email?.message}</p>。
xxxxxxxxxx641import { useForm } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34let renderCount = 0;56type formValues = {7 username: string8 email: string9 channel: string10}1112export const YoutubeForm = () => {1314 const form = useForm<formValues>();15 const { register, control, handleSubmit, formState } = form;16 const { errors } = formState;1718 const onSubmit = (data: formValues) => {19 console.log("Form Submit", data);20 }2122 renderCount++23 return (24 <div>25 {/* 这里为什么要展示renderCount / 2,因为在react项目中使用了StrictMode,组件在dev环境会重新渲染一次,数字就会变为2,如果不知道StrictMode的话,会引起误会 */}26 <h1>Youtube Form {renderCount / 2}</h1>27 <form onSubmit={handleSubmit(onSubmit)}>28 <div className="form-control">29 <label htmlFor="username"> Username </label>30 <input type="text" id="username" {register("username", {31 required: "Username is required"32 })} />33 <p className="error">{errors.username?.message}</p>34 </div>3536 <div className="form-control">37 <label htmlFor="email"> Email </label>38 <input type="text" id="email" {register("email", {39 pattern: {40 value: /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,41 message: "Invalid email format"42 }43 })} />44 <p className="error">{errors.email?.message}</p>45 </div>4647 <div className="form-control">48 <label htmlFor="channel"> Channel </label>49 <input type="text" id="channel" {register("channel", {50 required: {51 value: true,52 message: "Channel is required"53 }54 })} />55 <p className="error">{errors.channel?.message}</p>56 </div>5758 <button>Submit </button>59 </form>60 {/* 使用control将表单和DevTool关联起来 */}61 <DevTool control={control} />62 </div>63 );64};效果:

老师展示错误信息是这样写的<p className="error">{errors.username?.message}</p>,但是我经常看到是这样写的:
xxxxxxxxxx31{2 errors.username && <p className="error">{errors.username.message}</p>3}两种都可以。
这节课讲解怎么添加自定义校验规则,比如说在email输入框里面,我们想阻止用户输入admin@example.com这个邮箱,怎么做?
通过向register的第二个参数,添加第二个参数的方式来实现,key的名称为validate,value是一个函数。函数接收当前字段值为参数。
custom validation规则与pattern规则是分开的,互不影响,当然我的规则要设置的互不影响。
xxxxxxxxxx131<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: (fieldValue) => {9 return fieldValue !== "admin@example.com" || "Enter a different email address"10 }11 })} />12 <p className="error">{errors.email?.message}</p>13</div>return返回true就表示校验通过,就不会显示提供的错误信息,否则就显示错误信息。那么这里的return返回值的意思是:当fieldValue等于admin@example.com时,展示错误信息。
效果:

validate的值如果是一个对象,那么里面的key表示的就是规则的名称,这个可以自定义;value表示校验规则,是一个函数,与基本用法里面的函数一致。
xxxxxxxxxx201<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 <p className="error">{errors.email?.message}</p>20</div>效果:

之前我们学习了react hook form的基本用法,如下:

接下来,我们会更加深入的学习react hook form的相关概念,学习更多的特性:

这节课学习如何为form里面的field设置默认值。
在useForm()中传入一个对象,对象里面有一个属性defaultValues,在这里设置。
xxxxxxxxxx81const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: ""7 }8});效果:

比如说编辑时,进入这个表单之后,需要将保存的数据回显出来,怎么做?
使用https://jsonplaceholder.typicode.com/users/1提供的数据,回显里面的email。

将defaultValues改为一个函数,在里面请求数据,返回数据对象。
xxxxxxxxxx181const form = useForm<formValues>({2 // defaultValues: {3 // // key表示fieldname,value表示想要设置的默认值4 // username: "Batman",5 // email: "",6 // channel: ""7 // }8 defaultValues: async () => {9 const response = await fetch("https://jsonplaceholder.typicode.com/users/1")10 const data = await response.json();1112 return {13 username: "Batman",14 email: data.email,15 channel: ""16 }17 }18 });效果:

有时候我们想将某些字段分组在一起,这时候可以将这些字段放到一个对象里面,
1、定义typescript类型
xxxxxxxxxx91type formValues = {2 username: string3 email: string4 channel: string5 social: {6 twitter: string7 facebook: string8 }9}2、定义默认值
xxxxxxxxxx121const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: "",7 social: {8 twitter: "",9 facebook: ""10 }11 }12});3、添加jsx代码,注册fieldname。重点是注册的方式{...register("social.twitter")},真的是很方便啊。
xxxxxxxxxx91<div className="form-control">2 <label htmlFor="twitter"> Twitter </label>3 <input type="text" id="twitter" {register("social.twitter")} />4</div>56<div className="form-control">7 <label htmlFor="facebook"> Facebook </label>8 <input type="text" id="facebook" {register("social.facebook")} />9</div>
可以看到,提交的数据里面有social相关数据。
将表单空间的值作为数组来管理,也是很常见的,比如说多选框。react hook form也支持管理数组。
案例:收集用户的主要和次要手机号,不需要区分它们,只需要手机即可。(这里其实我不是很认同,因为主要和次要手机号都是要回显的,这里可以把它当作用户的爱好这类的信息)
1、定义typescript类型
xxxxxxxxxx101type formValues = {2 username: string3 email: string4 channel: string5 social: {6 twitter: string7 facebook: string8 }9 phoneNumbers: string[]10}2、设置默认值
xxxxxxxxxx131const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: "",7 social: {8 twitter: "",9 facebook: ""10 },11 phoneNumbers: ["", ""]12 }13});3、添加jsx,注册fieldname。重点就是注册方式{...register("phoneNumbers.0")},注意这种方式,是react hook form定义的方式,这里不能使用方括号。
xxxxxxxxxx91<div className="form-control">2 <label htmlFor="primary-phone"> Primary phone number </label>3 <input type="text" id="primary-phone" {register("phoneNumbers.0")} />4</div>56<div className="form-control">7 <label htmlFor="secondary-phone"> Secondary phone number </label>8 <input type="text" id="secondary-phone" {register("phoneNumbers.1")} />9</div>可以看到输出结果里面phoneNumbers数组,数据是正常的。

课后练习,为social和phoneNumbers相关的field添加required校验。
required还是正常设置,重点是错误提示信息,怎么展示。nested object这个简单,但是数组里面,怎么做?要这样做errors.phoneNumbers?.[1]?.message,是不是没有见过?其实应该这么看:
?.[1]:
作用:
?. (可选链) 确保 phoneNumbers 不是 null 或 undefined。[1] 使用方括号表示法访问 phoneNumbers 这个数组中索引为 1 的元素。结果: 得到数组中的第二个元素,它是一个错误对象(即 { "message": "...", "type": "..." })。
xxxxxxxxxx431<div className="form-control">2 <label htmlFor="twitter"> Twitter </label>3 <input type="text" id="twitter" {register("social.twitter", {4 required: {5 value: true,6 message: "Twitter is required"7 }8 })} />9 <p className="error">{errors.social?.twitter?.message}</p>10</div>1112<div className="form-control">13 <label htmlFor="facebook"> Facebook </label>14 <input type="text" id="facebook" {register("social.facebook", {15 required: {16 value: true,17 message: "Facebook is required"18 }19 })} />20 <p className="error">{errors.social?.facebook?.message}</p>21</div>2223<div className="form-control">24 <label htmlFor="primary-phone"> Primary phone number </label>25 <input type="text" id="primary-phone" {register("phoneNumbers.0", {26 required: {27 value: true,28 message: "Primary phone number is required"29 }30 })} />31 <p className="error">{errors.phoneNumbers?.[0]?.message}</p>32</div>3334<div className="form-control">35 <label htmlFor="secondary-phone"> Secondary phone number </label>36 <input type="text" id="secondary-phone" {register("phoneNumbers.1", {37 required: {38 value: true,39 message: "Secondary phone number is required"40 }41 })} />42 <p className="error">{errors.phoneNumbers?.[1]?.message}</p>43</div>效果:

如果是一个table里面,每一行有多个输入框,table可以CRUD,这时候应该怎么做呢?
因为数组长度是不固定的,此时可以使用
useFieldArray来处理。
上节课学习了arrays,但有时候用户需要输入的手机号、爱好等等数量是不固定的,这时候需要添加或删除字段,怎么办?
使用useFieldArray()。
1、引入useFieldArray,定义typescript类型
xxxxxxxxxx151import { useForm, useFieldArray } from "react-hook-form";23type formValues = {4 username: string5 email: string6 channel: string7 social: {8 twitter: string9 facebook: string10 }11 phoneNumbers: string[]12 phNumbers: {13 number: string14 }[]15}2、设置默认值
xxxxxxxxxx141const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: "",7 social: {8 twitter: "",9 facebook: ""10 },11 phoneNumbers: ["", ""],12 phNumbers: [{ number: "" }]13 }14}3、使用useFieldArray
xxxxxxxxxx71// 返回的fields是一个数组,要使用在jsx中2const { fields } = useFieldArray({3 // Name of the field array4 name: "phNumbers",5 // control object provided by useForm6 control: control7})4、添加jsx
xxxxxxxxxx141<div>2 <label>List of phone numbers</label>3 <div>4 {5 fields.map((field, index) => (6 <div className="form-control" key={field.id}>7 <input type="text" {8 register(`phNumbers.${index}.number` as const)9 } />10 </div>11 ))12 }13 </div>14</div>5、添加新增、删除按钮,使用useFieldArray提供的方法来处理
https://react-hook-form.com/docs/usefieldarray

xxxxxxxxxx61const { fields, append, remove } = useFieldArray({2 // Name of the field array3 name: "phNumbers",4 // control object provided by useForm5 control: control6})xxxxxxxxxx241<div>2 <label>List of phone numbers</label>3 <div>4 {5 fields.map((field, index) => (6 <div className="form-control" key={field.id}>7 <input type="text" {8 register(`phNumbers.${index}.number` as const)9 } />10 {11 index > 0 && (12 <button type="button" onClick={() => remove(index)}>13 Remove14 </button>15 )16 }17 </div>18 ))19 }20 <button type="button" onClick={() => append({ number: "" })}>21 Add phone number22 </button>23 </div>24</div>效果:

这节课学习处理数字和日期类型的数据。
1、定义typescript类型
xxxxxxxxxx141type formValues = {2 username: string3 email: string4 channel: string5 social: {6 twitter: string7 facebook: string8 }9 phoneNumbers: string[]10 phNumbers: {11 number: string12 }[]13 age: number14}2、设置默认值
xxxxxxxxxx151const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: "",7 social: {8 twitter: "",9 facebook: ""10 },11 phoneNumbers: ["", ""],12 phNumbers: [{ number: "" }],13 age: 014 }15});3、添加jsx
xxxxxxxxxx101<div className="form-control">2 <label htmlFor="age"> Age </label>3 <input type="number" id="age" {register("age", {4 required: {5 value: true,6 message: "Age is required"7 }8 })} />9 <p className="error">{errors.age?.message}</p>10</div>填写所有信息,提交后查看控制台输出,会发现age的值是string类型:

为了能让这个值变为数字类型,可以使用react hook form提供的valueAsNumber: number。

再看输出结果,会发现变为数字类型了。

1、定义typescript类型
xxxxxxxxxx151type formValues = {2 username: string3 email: string4 channel: string5 social: {6 twitter: string7 facebook: string8 }9 phoneNumbers: string[]10 phNumbers: {11 number: string12 }[]13 age: number14 dob: Date15}2、设置默认值
xxxxxxxxxx161const form = useForm<formValues>({2 defaultValues: {3 // key表示fieldname,value表示想要设置的默认值4 username: "Batman",5 email: "",6 channel: "",7 social: {8 twitter: "",9 facebook: ""10 },11 phoneNumbers: ["", ""],12 phNumbers: [{ number: "" }],13 age: 0,14 dob: new Date()15 }16});3、添加jsx
xxxxxxxxxx101<div className="form-control">2 <label htmlFor="dob"> Date of birth </label>3 <input type="date" id="dob" {register("dob", {4 required: {5 value: true,6 message: "Date of birth is required"7 }8 })} />9 <p className="error">{errors.dob?.message}</p>10</div>可以看到,提交的数据里面,dob是字符串类型。

有时候后端不需要这种类型,需要的是原始的Date类型,这时候可以使用valueAsDate: true来将数据转为Date类型。

可以看到,dob转为了Date类型:

需要注意的是,数据类型转换发生在校验之前,所以校验规则需要根据实际数据类型来编写。
这节课学习怎么watch组件中使用的fieldvalue。
https://react-hook-form.com/docs/useform/watch

watch是从useForm解构出来的,可以监听一个或多个fieldvalues。
1、监听一个fieldvalue
xxxxxxxxxx351import { useForm, useFieldArray } from "react-hook-form";2import { DevTool } from '@hookform/devtools'34let renderCount = 0;56export const YoutubeForm = () => {78 const form = useForm<formValues>({9 defaultValues: {10 11 }12 });13 14 // 解构出 watch15 const { register, control, handleSubmit, formState, watch } = form;16 const { errors } = formState;1718 // 监听返回值watchUsername19 const watchUsername = watch("username")2021 renderCount++22 return (23 <div>24 <h1>Youtube Form {renderCount / 2}</h1>25 26 {/* 展示监听的信息 */}27 <h2>Watched value: {watchUsername}</h2>2829 <form onSubmit={handleSubmit(onSubmit)}>30 ..........31 </form>32 <DevTool control={control} />33 </div>34 );35};效果:

2、监听多个fieldvalues
xxxxxxxxxx21 // 监听多个fieldvalues,传递一个数组给watch2 const [watchUsername, watchEmail] = watch(["username", "email"])效果:

3、监听整个表单
watch不传参就表示监听整个表单,返回值是一个对象。
xxxxxxxxxx11const watchFormdata = watch()效果:

4、执行watch的副作用
需要结合useEffect一起使用。
xxxxxxxxxx131import { useEffect } from "react";234const { register, control, handleSubmit, formState, watch } = form;5const { errors } = formState;67useEffect(() => {8 const subscription = watch((value) => {9 console.log(value);10 })1112 return () => subscription.unsubscribe();13}, [watch])可以看到,在监听的时候,renderCount的值没有发生变化,说明页面并没有rerender,这样的监听对性能非常友好。

注意:
1、2、3的监听方法,都触发了页面的rerender,使用的时候需要注意。
4的监听方法,没有触发页面的rerender,但是监听的是整个form表单。