一.react-hook-from是什么?
a.是什么?
你可以把它理解成一个专门帮你管表单的智能表格管理员。
它会帮你做这些事:
- 记住每个输入框的值
- 检查用户有没有填对
- 收集所有数据
- 在你点提交时统一交给你
- 告诉你哪里出错了
所以,react-hook-form 本质上就是:
一个专门管理 React 表单的库。
b.为什么需要它?
简单的说,就是为了更简单的去管理表单,否则填一个内容我们就需要使用useState来定义一个状态。然后错误的话也要自己去定义字段,还有就是校验也需要一个个写。比如下面的代码,只是定义了一点状态,还没有错误的,也没有校验,但是代码已经很多了。
jsx
import { useState } from "react";
function App() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({
username,
email,
password,
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
type="password"
/>
<button type="submit">提交</button>
</form>
);
}
二.如何使用?
a.简单使用:
先安装包
jsx
npm install react-hook-form
直接看最简单的代码:
jsx
import { useForm } from "react-hook-form"; // 从 react-hook-form 里引入 useForm 这个工具
function App() {
const { register, handleSubmit } = useForm(); // 创建表单管理器,并拿出 register 和 handleSubmit 两个常用功能
const onSubmit = (data) => { // 定义一个函数,表单提交成功后会执行它
console.log(data); // 把表单收集到的数据打印到控制台
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 表单提交时,先交给 handleSubmit 处理,再调用 onSubmit */}
<input {...register("username")} placeholder="请输入用户名" /> {/* 注册一个输入框,名字叫 username */}
<input {...register("password")} type="password" placeholder="请输入密码" /> {/* 注册一个密码输入框,名字叫 password */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
1)useForm
这是整个表单的"总入口"。
你可以理解为:
你要使用 _react-hook-form_,就先去领一个"表单管理器"。
2)register
这个是把输入框"登记"到表单系统里。
你可以理解为:
告诉这个工具:"这个输入框归你管了。"
1....register("username") 里的三个点是什么意思?
这是新手常见疑问,我用大白话说。
jsx
<input {...register("username")} />
这里的 ... 叫"展开"。
你先不用记专业定义,你可以理解为:
_register("username")_会返回一包这个输入框需要的配置,\ _..._就是把这一包配置摊开,放进这个 input 里。
其实就像:
- 你买了一套"输入框管理配件"
register("username")把配件包给你...帮你把配件安装到这个 input 上
你现在先知道它是"固定写法"就够了,后面你会越来越熟。\ 2.如果没有写register
那这个输入框就没有注册到表单系统里。
结果就是:
- 页面上能输入
- 但提交时拿不到它的值
你可以理解为:
这个人填了表,但没登记进系统,所以最后不会被统计进去。
3)handleSubmit
这个是提交时帮你收集数据、处理提交的。
你可以理解为:
用户点提交时,它先把表单整理好,再交给你。
4)formState.errors(这里还没用到,先列出来)
这个是专门放错误信息的地方。
你可以理解为:
哪个输入框出错了,都能在这里找到。
b.表单校验(必填、长度限制、错误提示)
1.写法
规则通常写在 register() 里面。
比如:
jsx
<JSX>register("username", { required: "用户名不能为空" })
你可以理解为:
- 第一个参数
"username":字段名字 - 第二个参数
{ required: "用户名不能为空" }:这个字段的规则
就像你在登记表旁边贴了一张说明:
- 这一栏必须填
- 这一栏最少几位
- 不符合就提示什么
2.例子(必填)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 用来注册输入框
handleSubmit, // 用来处理表单提交
formState: { errors }, // 从表单状态里取出 errors,用来拿错误信息
} = useForm(); // 创建表单管理器
const onSubmit = (data) => { // 定义提交成功后的函数
console.log("提交成功,数据是:", data); // 打印表单数据
};
return (
<div>
<h1>注册表单</h1> {/* 页面标题 */}
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交时先经过 react-hook-form 处理 */}
<div>
<input
{...register("username", { required: "用户名不能为空" })} // 注册 username,并设置必填规则
placeholder="请输入用户名" // 输入框提示文字
/>
{errors.username && <p>{errors.username.message}</p>} {/* 如果 username 有错误,就显示错误信息 */}
</div>
<div>
<input
{...register("password", { required: "密码不能为空" })} // 注册 password,并设置必填规则
type="password" // 设置为密码输入框
placeholder="请输入密码" // 输入框提示文字
/>
{errors.password && <p>{errors.password.message}</p>} {/* 如果 password 有错误,就显示错误信息 */}
</div>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
</div>
);
}
export default App; // 导出组件
plain
{errors.username && <p>{errors.username.message}</p>}
你可以直接把它理解成一句人话:
如果 _username_有错误,就显示错误提示。
3.例子(最小长度,常见的检验有现成的api配置)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 处理提交
formState: { errors }, // 获取错误信息
} = useForm(); // 创建表单工具
const onSubmit = (data) => { // 提交成功函数
console.log("提交成功,数据是:", data); // 打印表单数据
};
return (
<div>
<h1>注册表单</h1> {/* 标题 */}
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<div>
<input
{...register("username", {
required: "用户名不能为空", // 用户名必须填写
})}
placeholder="请输入用户名" // 提示文字
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示用户名错误 */}
</div>
<div>
<input
{...register("password", {
required: "密码不能为空", // 密码必须填写
minLength: {
value: 6, // 最少 6 位
message: "密码长度不能少于 6 位", // 不满足时的提示
},
})}
type="password" // 密码框
placeholder="请输入密码" // 提示文字
/>
{errors.password && <p>{errors.password.message}</p>} {/* 显示密码错误 */}
</div>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
</div>
);
}
export default App; // 导出组件
required 可以这样写
plain
<JSX>required: "密码不能为空"
minLength 常这样写
plain
<JSX>minLength: {
value: 6,
message: "密码长度不能少于 6 位"
}
你可以理解为:
required只需要表达"要不要填"minLength需要表达"最少多少位",所以要多写一个value
也就是说:
value:规则本身message:报错提示
4.例子(邮箱验证,正则检验)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 处理提交
formState: { errors }, // 获取错误信息
} = useForm(); // 创建表单管理器
const onSubmit = (data) => { // 提交成功函数
console.log("提交成功,数据是:", data); // 打印数据
};
return (
<div>
<h1>邮箱表单</h1> {/* 标题 */}
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<div>
<input
{...register("email", {
required: "邮箱不能为空", // 邮箱必须填写
pattern: {
value: /^\S+@\S+\.\S+$/, // 一个简单的邮箱格式检查规则
message: "邮箱格式不正确", // 格式错误时的提示
},
})}
placeholder="请输入邮箱" // 提示文字
/>
{errors.email && <p>{errors.email.message}</p>} {/* 显示邮箱错误 */}
</div>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
</div>
);
}
export default App; // 导出组件
怎么理解 pattern?
你现在可以把它理解成:
一个"格式检查器"。
5.例子(最大长度,最大值,最小值等)
- maxLength:最多多少位
jsx
maxLength: {
value: 10,
message: "用户名最多 10 位"
}
2)min:最小值
jsx
min: {
value: 18,
message: "年龄不能小于 18 岁"
}
3)max:最大值
jsx
max: {
value: 60,
message: "年龄不能大于 60 岁"
}
6.例子(重头戏:自定义校验 validate)
1)确认密码例子
确认密码必须和密码完全一样的例子\ 前置知识:\ 在做"确认密码"时,我们需要先知道"密码输入框现在的值是什么"。
这时候就要用 watch。
你可以把 watch 理解成:
偷看一下某个字段当前输入的内容
例子
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 处理提交
watch, // 监听某个字段当前的值
formState: { errors }, // 获取错误信息
} = useForm(); // 创建表单工具
const passwordValue = watch("password"); // 读取 password 输入框当前的值
const onSubmit = (data) => { // 提交成功函数
console.log("提交成功,数据是:", data); // 打印数据
};
return (
<div>
<h1>注册表单</h1> {/* 页面标题 */}
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<div>
<input
{...register("password", {
required: "密码不能为空", // 密码必填
minLength: {
value: 6, // 最少 6 位
message: "密码长度不能少于 6 位", // 错误提示
},
})}
type="password" // 密码输入框
placeholder="请输入密码" // 提示文字
/>
{errors.password && <p>{errors.password.message}</p>} {/* 显示密码错误 */}
</div>
<div>
<input
{...register("confirmPassword", {
required: "请再次输入密码", // 确认密码必填
validate: (value) => {
return value === passwordValue || "两次输入的密码不一致"; // 如果一致返回 true,不一致返回错误提示
},
})}
type="password" // 确认密码输入框
placeholder="请再次输入密码" // 提示文字
/>
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>} {/* 显示确认密码错误 */}
</div>
<button type="submit">注册</button> {/* 提交按钮 */}
</form>
</div>
);
}
export default App; // 导出组件
2)用户名不能是 admin
jsx
validate: (value) => {
if (value === "admin") {
return "用户名不能是 admin";
}
return true;
}
3)优惠码必须以 VIP 开头
jsx
validate: (value) => {
if (value.startsWith("VIP")) {
return true;
}
return "优惠码必须以 VIP 开头";
}
c.默认值、重置表单、获取当前表单值
1)defaultValues
像什么?
像老师提前帮你把表格的一部分内容填好了。
比如:
姓名:小明
你打开表单时,这些内容已经在里面了。
这就叫默认值。
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
username: "小明", // username 默认值是 小明
email: "xiaoming@test.com", // email 默认值是 xiaoming@test.com
},
}); // 创建表单工具,并设置默认值
const onSubmit = (data) => { // 提交成功函数
console.log("提交的数据是:", data); // 打印表单数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<input {...register("username")} placeholder="请输入用户名" /> {/* 注册 username 输入框 */}
<input {...register("email")} placeholder="请输入邮箱" /> {/* 注册 email 输入框 */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
2)reset()
像什么?
像你按下"重新填写"按钮,把整张表恢复到初始状态。
比如:
清空所有输入框
或者恢复成最初默认值
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, handleSubmit, reset } = useForm({
defaultValues: {
username: "小明", // 用户名默认值
email: "xiaoming@test.com", // 邮箱默认值
},
}); // 创建表单工具,并设置默认值
const onSubmit = (data) => { // 提交成功函数
console.log("提交的数据是:", data); // 打印数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<input {...register("username")} placeholder="请输入用户名" /> {/* 用户名输入框 */}
<input {...register("email")} placeholder="请输入邮箱" /> {/* 邮箱输入框 */}
<button type="submit">提交</button> {/* 提交按钮 */}
<button type="button" onClick={() => reset()}>重置</button> {/* 点击后重置表单 */}
</form>
);
}
export default App; // 导出组件
reset() 有两种常见用法:
用法 1:恢复初始状态
jsx
reset();
用法 2:重置为你指定的新值
jsx
reset({
username: "新用户",
email: "new@test.com"
});
3)getValues()
像什么?
像你在交表前,偷偷低头看一眼:"我现在都填了什么?"
也就是说:
不提交,也能先拿到表单当前的内容。
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, getValues } = useForm(); // 创建表单工具,并取出 getValues
const handleCheck = () => { // 定义一个查看当前表单值的函数
const values = getValues(); // 获取当前表单所有值
console.log("当前表单的值是:", values); // 打印当前值
};
return (
<div>
<input {...register("username")} placeholder="请输入用户名" /> {/* 用户名输入框 */}
<input {...register("email")} placeholder="请输入邮箱" /> {/* 邮箱输入框 */}
<button type="button" onClick={handleCheck}>查看当前值</button> {/* 点击后查看当前表单值 */}
</div>
);
}
export default App; // 导出组件
getValues("username") 还能只拿一个字段
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, getValues } = useForm(); // 创建表单工具
const handleCheckUsername = () => { // 查看用户名的函数
const username = getValues("username"); // 获取 username 当前值
console.log("当前用户名是:", username); // 打印用户名
};
return (
<div>
<input {...register("username")} placeholder="请输入用户名" /> {/* 用户名输入框 */}
<input {...register("email")} placeholder="请输入邮箱" /> {/* 邮箱输入框 */}
<button type="button" onClick={handleCheckUsername}>查看用户名</button> {/* 点击后查看用户名 */}
</div>
);
}
export default App; // 导出组件
d.setValue、watch,以及表单联动
1)watch()
用来"观察"某个字段当前的值
你可以理解为:
随时偷看这个输入框里现在写了什么
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, watch } = useForm(); // 创建表单工具,并取出 register 和 watch
const username = watch("username"); // 实时获取 username 输入框当前的值
return (
<div>
<h1>实时预览</h1> {/* 标题 */}
<input
{...register("username")} // 注册 username 输入框
placeholder="请输入用户名" // 提示文字
/>
<p>你正在输入:{username}</p> {/* 实时显示当前输入内容 */}
</div>
);
}
export default App; // 导出组件
2)setValue()
用来"主动修改"某个字段的值
你可以理解为:
我不等用户手动输入,我直接帮他把某个输入框改掉
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, setValue } = useForm(); // 创建表单工具,并取出 setValue
const fillName = () => { // 定义一个自动填充名字的函数
setValue("username", "小明"); // 把 username 输入框的值设置成 小明
};
return (
<div>
<h1>自动填充</h1> {/* 标题 */}
<input
{...register("username")} // 注册 username 输入框
placeholder="请输入用户名" // 提示文字
/>
<button type="button" onClick={fillName}>一键填入用户名</button> {/* 点击按钮自动填值 */}
</div>
);
}
export default App; // 导出组件
真实场景 1:勾选"同意协议"后才能提交
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, watch, handleSubmit } = useForm(); // 创建表单工具
const agree = watch("agree"); // 监听 agree 复选框当前是否勾选
const onSubmit = (data) => { // 提交成功函数
console.log("提交数据:", data); // 打印数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<input
{...register("username")} // 注册用户名输入框
placeholder="请输入用户名" // 提示文字
/>
<div>
<input
type="checkbox" // 复选框
{...register("agree")} // 注册 agree 字段
/>
<span>我已阅读并同意协议</span> {/* 提示文字 */}
</div>
<button type="submit" disabled={!agree}>提交</button> {/* 没勾选时不能提交 */}
</form>
);
}
export default App; // 导出组件
e.Controller
1)Controller 是什么,为什么需要它,怎么用它?
主要原因就是:register只适合最原始的表单元素,对于自定义组件,第三方组件使用register可能就会失效了。
2)如何使用Controller(基础使用)?
jsx
import { useForm, Controller } from "react-hook-form"; // 引入 useForm 和 Controller
function App() {
const { control, handleSubmit } = useForm(); // 创建表单工具,并取出 control
const onSubmit = (data) => { // 提交成功函数
console.log("提交的数据是:", data); // 打印提交数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<Controller
name="username" // 字段名叫 username
control={control} // 把 control 传给 Controller
render={({ field }) => ( // 告诉 Controller 最终渲染什么
<input
{...field} // 把 field 里的 value、onChange 等连接到 input 上
placeholder="请输入用户名" // 提示文字
/>
)}
/>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
上面的列子使用的是原生的input,使用register会更简单,但是这里主要是举例说明Controller怎么使用。
3)如何使用Controller(自定义组件):
jsx
import { useForm, Controller } from "react-hook-form"; // 引入 useForm 和 Controller
function MyInput({ value, onChange, placeholder }) { // 自定义输入组件
return (
<input
value={value} // 当前值
onChange={onChange} // 输入变化时触发
placeholder={placeholder} // 提示文字
/>
);
}
function App() {
const { control, handleSubmit } = useForm({
defaultValues: {
username: "", // username 默认值为空
},
}); // 创建表单工具
const onSubmit = (data) => { // 提交成功函数
console.log("提交的数据是:", data); // 打印数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<Controller
name="username" // 字段名
control={control} // 传入 control
render={({ field }) => ( // 渲染输入组件
<MyInput
value={field.value} // 把当前值传给自定义组件
onChange={field.onChange} // 把变化处理传给自定义组件
placeholder="请输入用户名" // 提示文字
/>
)}
/>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
4)如何使用Controller(配合校验使用)
jsx
import { useForm, Controller } from "react-hook-form"; // 引入 useForm 和 Controller
function MyInput({ value, onChange, placeholder }) { // 自定义输入组件
return (
<input
value={value} // 当前值
onChange={onChange} // 输入变化时触发
placeholder={placeholder} // 提示文字
/>
);
}
function App() {
const {
control, // Controller 需要的控制器
handleSubmit, // 提交处理
formState: { errors }, // 错误信息
} = useForm({
defaultValues: {
username: "", // 默认值
},
}); // 创建表单工具
const onSubmit = (data) => { // 提交成功函数
console.log("提交的数据是:", data); // 打印数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 提交表单 */}
<Controller
name="username" // 字段名
control={control} // 传入 control
rules={{ required: "用户名不能为空" }} // 校验规则
render={({ field }) => ( // 渲染组件
<MyInput
value={field.value} // 当前值
onChange={field.onChange} // 输入变化处理
placeholder="请输入用户名" // 提示文字
/>
)}
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示错误提示 */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
注意: 在 Controller 里,校验规则放在 rules 这个属性里
5)特殊组件核心模板(只要是自定义组件或者第三方组件):
jsx
<Controller
name="字段名"
control={control}
rules={{ 校验规则 }}
render={({ field }) => (
<你的组件
value={field.value}
onChange={field.onChange}
/>
)}
/>
f.formState 常用状态:提交成功 / 失败怎么处理
1)handleSubmit 不止一个回调
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 提交处理器
formState: { errors, isSubmitting, isSubmitted, isValid }, // 常用状态
} = useForm({
mode: "onChange", // 输入时就更新校验状态(这样 isValid 会实时变化)
});
const onValid = async (data) => { // 校验通过时调用
console.log("提交成功,数据是:", data); // 打印成功数据
await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟请求耗时 1 秒
alert("保存成功!"); // 提示成功
};
const onInvalid = (errorInfo) => { // 校验失败时调用
console.log("提交失败,错误是:", errorInfo); // 打印错误对象
alert("提交失败,请检查表单"); // 提示失败
};
return (
<form onSubmit={handleSubmit(onValid, onInvalid)}> {/* 提交时分别走成功/失败回调 */}
<input
{...register("username", { required: "用户名不能为空" })} // 注册 username 并设置必填
placeholder="请输入用户名" // 占位提示
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示用户名错误 */}
<input
type="password" // 密码输入框
{...register("password", {
required: "密码不能为空", // 必填
minLength: { value: 6, message: "密码至少 6 位" }, // 最短 6 位
})}
placeholder="请输入密码"
/>
{errors.password && <p>{errors.password.message}</p>} {/* 显示密码错误 */}
<button type="submit" disabled={isSubmitting}> {/* 提交中时禁用按钮 */}
{isSubmitting ? "提交中..." : "提交"} {/* 根据状态切换按钮文字 */}
</button>
<p>是否提交过:{isSubmitted ? "是" : "否"}</p> {/* 是否至少提交过一次 */}
<p>当前是否有效:{isValid ? "是" : "否"}</p> {/* 当前校验是否通过 */}
</form>
);
}
export default App; // 导出组件
2)formState 常用状态
a) isSubmitting
你可以理解为"正在提交中"。
true:正在请求(比如等接口返回)
false:没有在提交
常见用法:禁用按钮 + 显示"提交中..."。
b) isSubmitted
你可以理解为"是否提交过至少一次"。
提交过一次就会变成 true
常用于控制"提交后才显示某些提示"
c) isValid
你可以理解为"当前整张表单是否通过校验"。
true:所有规则都通过
false:还有字段不合格
小提醒:想让它"实时更新",常配 mode: "onChange"。
d) errors
保存每个字段的错误信息。
比如:
errors.username?.message
errors.password?.message
g.reset(重置表单)和 setValue(代码里手动改值)
jsx
import { useEffect } from "react"; // 引入 useEffect
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 提交处理
reset, // 重置整张表单
setValue, // 手动设置某个字段值
watch, // 观察字段当前值(方便演示)
formState: { errors }, // 错误信息
} = useForm({
defaultValues: { // 表单初始值
username: "", // 用户名默认空
email: "", // 邮箱默认空
},
});
const onSubmit = (data) => { // 提交成功回调
console.log("提交数据:", data); // 打印数据
};
useEffect(() => { // 组件加载后模拟"接口回填"
const timer = setTimeout(() => { // 1 秒后执行
reset({ username: "小明", email: "xiaoming@test.com" }); // 一次性回填整张表单
}, 1000);
return () => clearTimeout(timer); // 组件卸载时清理定时器
}, [reset]);
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<input
{...register("username", { required: "用户名不能为空" })} // 用户名必填
placeholder="用户名"
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示用户名错误 */}
<input
{...register("email", { required: "邮箱不能为空" })} // 邮箱必填
placeholder="邮箱"
/>
{errors.email && <p>{errors.email.message}</p>} {/* 显示邮箱错误 */}
<p>当前用户名:{watch("username")}</p> {/* 实时显示 username 值 */}
<button
type="button" // 普通按钮,不触发表单提交
onClick={() => setValue("username", "手动改成小红")} // 只修改 username
>
只改用户名(setValue)
</button>
<button
type="button" // 普通按钮
onClick={() => reset()} // 重置回 defaultValues
>
重置为初始值(reset)
</button>
<button
type="button" // 普通按钮
onClick={() => reset({ username: "游客", email: "" })} // 重置到你指定的新值
>
重置为"游客"
</button>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
1)使用场景
setValue("字段名", 新值)
只改一个字段
常用于:点击按钮快速填值、联动修改字段
reset(整包对象)
一次改整张表单
常用于:接口拿到数据后"回填编辑页"
2)常见坑点
a.把 setValue 当成 reset 用
setValue 是"点改";reset 是"整表恢复"。
b.reset() 后发现不是清空
因为它会回到 defaultValues,不是一定变空。
c.按钮忘记写 type="button"
在
里按钮默认是 submit,可能一点击就提交了。
d.接口回填时不用 reset,用一堆 setValue
小表单还行,大表单很累。回填整包数据优先 reset。
h.watch(监听输入变化)做实时预览与联动
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
watch, // 监听字段变化
handleSubmit, // 提交处理
} = useForm({
defaultValues: { // 默认值
nickname: "", // 昵称默认空
city: "北京", // 城市默认北京
},
});
const nickname = watch("nickname"); // 实时拿到 nickname 当前值
const city = watch("city"); // 实时拿到 city 当前值
const onSubmit = (data) => { // 提交回调
console.log("提交数据:", data); // 打印提交数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<input
{...register("nickname")} // 注册昵称字段
placeholder="请输入昵称"
/>
<select {...register("city")}> {/* 注册城市下拉框 */}
<option value="北京">北京</option> {/* 选项:北京 */}
<option value="上海">上海</option> {/* 选项:上海 */}
<option value="广州">广州</option> {/* 选项:广州 */}
</select>
<h3>实时预览:</h3> {/* 标题 */}
<p>昵称:{nickname || "(还没输入)"}</p> {/* 实时显示昵称 */}
<p>城市:{city}</p> {/* 实时显示城市 */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const { register, watch } = useForm(); // 解构 register 和 watch
const city = watch("city", "北京"); // 监听 city,默认北京
return (
<div>
<select {...register("city")}> {/* 注册城市 */}
<option value="北京">北京</option>
<option value="上海">上海</option>
</select>
{city === "北京" && <p>当前配送时效:次日达</p>} {/* 北京时的文案 */}
{city === "上海" && <p>当前配送时效:当日达</p>} {/* 上海时的文案 */}
</div>
);
}
export default App; // 导出组件
1)watch 常见用法(你先会这 3 个)
a. watch("字段名")
监听单个字段
b. watch(["a", "b"])
同时监听多个字段
c. watch()
监听整张表单(初学阶段少用,容易频繁刷新)
2)常见坑点
a.一上来就 watch() 全量监听
可能导致页面频繁更新。
建议先精准监听:watch("nickname")。
b.忘了给默认值,首次是 undefined
可以在 useForm({ defaultValues }) 里给,
或 watch("city", "北京") 给兜底值。
c.把 watch 当"提交数据"用
watch 是"实时看值",
真正提交仍然靠 handleSubmit(onSubmit)
i.Controller:对接"第三方输入组件"
jsx
interface FormMyDatePickerData {
birthday: string;
}
function ControllerUseMyDatePickerFrom() {
const {
control, // 给 Controller 用的控制器
handleSubmit, // 提交处理
formState: { errors }, // 错误信息
} = useForm<FormMyDatePickerData>({
defaultValues: {
birthday: "", // 生日默认值
},
});
const onSubmit = (data: FormMyDatePickerData) => { // 提交回调
console.log("提交数据:", data); // 打印数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<Controller
name="birthday" // 字段名
control={control} // 传入 control
rules={{ required: "请选择生日" }} // 校验规则
render={({ field }) => ( // field 里有 value/onChange/onBlur/name/ref
<MyDatePicker
value={field.value} // 把表单值传给组件
onChange={field.onChange} // 把组件变化同步回表单
/>
)}
/>
{errors.birthday && <p>{errors.birthday.message}</p>} {/* 显示错误 */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
1)Controller 你先记住这 4 个点
a.必须有 name
这是字段名字。
b.必须有 control
从 useForm() 里解构出来。
c.用 render 把字段"接线"给组件
重点是把 field.value、field.onChange 传进去。
d.校验规则写在 rules
跟 register 里的规则类似。
2)和 register 的区别(超简版)
原生表单控件:优先 register
第三方复杂组件:用 Controller
你可以理解为:
register 是"直连",Controller 是"转接头"。
3)坑点
a.忘传 control
Controller 就不会工作。
b.只传 value,没传 onChange
看起来有值,但改不了(或改了不同步)。
c.defaultValues 没给,出现受控/非受控警告
尤其是日期、Select 组件,建议给初始值。
d.第三方组件的 onChange 参数格式不一样
有些不是 event,而是直接给值对象。
这时你要手动转换后再 field.onChange(...)。
j.useFieldArray:动态增删表单项(重复表单块)
使用场景如下图:

1)基础示例
jsx
import { useForm, useFieldArray } from "react-hook-form"; // 引入 useForm 和 useFieldArray
function App() {
const { control, register, handleSubmit } = useForm({ // 创建表单
defaultValues: { // 默认值
contacts: [ // contacts 是一个数组字段
{ name: "", phone: "" }, // 默认先给一条联系人
],
},
});
const { fields, append, remove } = useFieldArray({ // 管理 contacts 这个数组
control, // 传入 control
name: "contacts", // 指定数组字段名
});
const onSubmit = (data) => { // 提交回调
console.log("提交数据:", data); // 打印最终数据
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<h3>联系人列表</h3> {/* 标题 */}
{fields.map((item, index) => ( // 遍历每一条联系人
<div key={item.id} style={{ marginBottom: 12 }}> {/* key 必须用 item.id */}
<input
placeholder="姓名"
{...register(`contacts.${index}.name`)} // 注册第 index 条的 name
/>
<input
placeholder="手机号"
{...register(`contacts.${index}.phone`)} // 注册第 index 条的 phone
/>
<button
type="button" // 普通按钮,避免触发表单提交
onClick={() => remove(index)} // 删除当前这一条
>
删除
</button>
</div>
))}
<button
type="button" // 普通按钮
onClick={() => append({ name: "", phone: "" })} // 新增一条空联系人
>
新增联系人
</button>
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
);
}
export default App; // 导出组件
2)核心api
a.fields
当前"可渲染的列表项"
你用它 map 出多组输入框
b.append(新对象)
在末尾新增一条
c.remove(index)
删除某一条
3)坑点:
a.key 用了 index,没用 item.id
可能导致输入框错位、值串行。
记住:key={item.id}。
b.字段路径写错
要写成:contacts.${index}.name 这种格式。
少一个点都不行。
c.新增按钮没写 type="button"
会误触发表单提交。
d.append 的对象结构不完整
你有 name/phone 两个字段,就尽量都给上初始值。
k.reset(编辑页回填实战)
1)使用场景(编辑回填)
你可以理解为这个场景:
- 新增页:表单是空的
- 编辑页:要先请求旧数据,再把旧数据"塞回表单"让用户改
在 React Hook Form 里,最常用的方法就是:**reset()**
jsx
import { useEffect } from "react"; // 引入 useEffect
import { useForm, useFieldArray } from "react-hook-form"; // 引入 hooks
function App() {
const {
control, // 给 useFieldArray 和 Controller 用
register, // 注册输入框
handleSubmit, // 提交处理
reset, // 回填核心:重置整张表单
} = useForm({
defaultValues: { // 先给一个基础结构,避免初始 undefined
username: "", // 用户名默认空
email: "", // 邮箱默认空
contacts: [{ name: "", phone: "" }], // 默认一条联系人
},
});
const { fields, append, remove } = useFieldArray({
control, // 传入 control
name: "contacts", // 数组字段名
});
useEffect(() => {
async function fetchUserDetail() { // 模拟请求编辑详情
const apiData = await Promise.resolve({ // 假装接口返回的数据
username: "小明", // 接口用户名
email: "xiaoming@test.com", // 接口邮箱
contacts: [ // 接口联系人数组
{ name: "妈妈", phone: "13800000000" },
{ name: "同事", phone: "13900000000" },
],
});
reset(apiData); // 一次性把接口数据回填到整张表单
}
fetchUserDetail(); // 页面加载后执行
}, [reset]); // 依赖 reset
const onSubmit = (data) => { // 提交回调
console.log("提交数据:", data); // 打印最终提交内容
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<h3>编辑用户</h3>
<input
placeholder="用户名"
{...register("username")} // 注册 username
/>
<input
placeholder="邮箱"
{...register("email")} // 注册 email
/>
<h4>联系人</h4>
{fields.map((item, index) => (
<div key={item.id}> {/* key 必须用 item.id */}
<input
placeholder="联系人姓名"
{...register(`contacts.${index}.name`)} // 第 index 条联系人姓名
/>
<input
placeholder="联系人电话"
{...register(`contacts.${index}.phone`)} // 第 index 条联系人电话
/>
<button type="button" onClick={() => remove(index)}>删除</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "", phone: "" })} // 新增联系人
>
新增联系人
</button>
<button type="submit">保存</button>
</form>
);
}
export default App; // 导出组件
2)关键点
a.接口数据到了以后,用 reset(data) 回填
最省心,整表更新。
b.defaultValues 先给"形状"
尤其是数组字段(如 contacts),先给结构更稳。
c.动态数组也能跟着 reset 一起回填
只要 data.contacts 是数组,useFieldArray 会正确渲染条数。
3)坑点:
a.把回填写成一堆 setValue
不是不行,但字段多时很痛苦。
编辑页优先 reset。
b.接口字段名和表单字段名对不上
比如接口是 user_name,表单是 username。
需要先做一层映射再 reset。
c.接口返回 contacts: null 导致报错
你可以兜底:contacts: api.contacts ?? []。
d.先渲染后回填时出现"闪一下空表单"
常见现象。可加 loading,数据到位再显示表单。
4)总结:
编辑页回填的标准动作:__请求详情 → 数据映射 → _**reset(mappedData)**_
l.isSubmitting:提交状态与防重复提交(按钮 loading处理 + 错误处理)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 表单提交处理器
setError, // 手动设置字段错误
formState: { errors, isSubmitting }, // errors 错误对象,isSubmitting 提交中状态
} = useForm({
defaultValues: { // 默认值
username: "", // 用户名默认空
password: "", // 密码默认空
},
});
const onSubmit = async (data) => { // 提交函数(异步)
try {
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟网络请求 1.5 秒
if (data.username === "admin") { // 模拟后端返回"用户名已存在"
setError("username", { type: "server", message: "这个用户名已被占用" }); // 把后端错误显示到字段上
return; // 终止后续逻辑
}
alert("提交成功!"); // 成功提示
} catch (err) {
alert("网络异常,请稍后重试"); // 全局失败提示
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<div>
<input
placeholder="用户名"
{...register("username", { required: "请输入用户名" })} // 必填校验
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示用户名错误 */}
</div>
<div>
<input
type="password"
placeholder="密码"
{...register("password", { required: "请输入密码", minLength: { value: 6, message: "至少6位" } })} // 密码校验
/>
{errors.password && <p>{errors.password.message}</p>} {/* 显示密码错误 */}
</div>
<button type="submit" disabled={isSubmitting}> {/* 提交中禁用按钮,防重复提交 */}
{isSubmitting ? "提交中..." : "登录"} {/* 根据状态切换文案 */}
</button>
</form>
);
}
export default App; // 导出组件
1)核心关键点
a.isSubmitting
提交开始后自动变 true
提交结束后自动回 false
b.按钮 disabled={isSubmitting}
防止用户重复点击
c.错误分两类处理
字段错误:setError("字段名", { message: "..." })
全局错误:toast / alert(比如网络错误)
2)坑点
a.忘了把 onSubmit 写成 async
isSubmitting 的体验会不对。
b.按钮没禁用
用户连点触发多次请求,可能生成重复数据。
c.后端报错只 console.log,不反馈给用户
用户不知道发生了什么,会反复点。
d.把"字段错误"和"全局错误"混在一起
建议区分:
哪个输入框有问题 → 显示在输入框下
系统挂了/超时 → 全局提示
3)总结
专业表单提交 = _isSubmitting_控状态 + 禁用按钮防连点 + 给用户清晰错误反馈。
j.提交后把后端错误显示在页面上
1)使用setError回显
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 提交处理
setError, // 手动设置字段错误
formState: { errors, isSubmitting }, // 取错误和提交状态
} = useForm({
defaultValues: { // 默认值
email: "", // 邮箱
phone: "", // 手机号
password: "", // 密码
},
});
const fakeApi = async (data) => { // 模拟请求函数
await new Promise((r) => setTimeout(r, 1000)); // 模拟网络延迟
return { // 模拟后端返回失败
ok: false, // 请求结果失败
message: "校验失败", // 全局消息
errors: { // 字段级错误
email: "邮箱已被注册",
phone: "手机号格式不正确",
},
};
};
const onSubmit = async (formData) => { // 提交函数
const res = await fakeApi(formData); // 调接口
if (!res.ok) { // 如果失败
if (res.errors) { // 如果有字段错误
Object.entries(res.errors).forEach(([field, message]) => { // 遍历错误对象
setError(field, { type: "server", message }); // 映射到 RHF 字段错误
});
}
if (res.message) { // 如果有全局消息
alert(res.message); // 给一个全局提示
}
return; // 结束
}
alert("提交成功"); // 成功提示
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<div>
<input
placeholder="邮箱"
{...register("email", {
required: "请输入邮箱", // 前端必填
pattern: { value: /^\S+@\S+\.\S+$/, message: "邮箱格式不正确" }, // 前端格式校验
})}
/>
{errors.email && <p>{errors.email.message}</p>} {/* 展示邮箱错误 */}
</div>
<div>
<input
placeholder="手机号"
{...register("phone", { required: "请输入手机号" })} // 前端必填
/>
{errors.phone && <p>{errors.phone.message}</p>} {/* 展示手机号错误 */}
</div>
<div>
<input
type="password"
placeholder="密码"
{...register("password", { required: "请输入密码", minLength: { value: 6, message: "至少6位" } })} // 密码校验
/>
{errors.password && <p>{errors.password.message}</p>} {/* 展示密码错误 */}
</div>
<button type="submit" disabled={isSubmitting}> {/* 提交中禁用 */}
{isSubmitting ? "提交中..." : "注册"} {/* 文案切换 */}
</button>
</form>
);
}
export default App; // 导出组件
2)关键理解
a.前端校验负责"即时体验"
比如空值、长度、格式,用户输入时就能知道。
b.后端校验负责"最终正确性"
比如"邮箱是否已存在"这种必须问数据库的规则,只能后端判断。
c.后端错误要"按字段落地"
setError("email", { message: "..." })
这样用户一眼就知道该改哪个框。
3)常见坑点
a.后端字段名和前端字段名不一致
后端 user_email,前端 email。
需要做映射表,不然 setError 对不上。
b.只弹 toast,不显示字段错误
用户知道"失败了",但不知道改哪里。
c.后端返回数组错误,没处理
有些接口返回 errors: [{field, message}],要先转换成对象或循环设置。
d.把后端错误覆盖前端交互节奏
建议:先过前端校验,再请求后端。体验更顺滑。
4)总结
前后端校验协同的核心:__前端做快反馈,后端做最终裁决,错误用 _**setError**_精准贴回字段。
k.watch 与联动表单(一个字段变化,带动另一个字段)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
function App() {
const {
register, // 注册输入框
handleSubmit, // 提交处理器
watch, // 监听字段变化
formState: { errors }, // 取错误对象
} = useForm({
defaultValues: { // 默认值
needInvoice: "no", // 默认不开发票
invoiceTitle: "", // 发票抬头
taxNo: "", // 税号
},
});
const needInvoice = watch("needInvoice"); // 实时监听 needInvoice 当前值
const onSubmit = (data) => { // 提交函数
console.log("提交数据:", data); // 打印数据
alert(JSON.stringify(data, null, 2)); // 弹窗查看结果
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<h3>订单信息</h3>
<label>
<input
type="radio"
value="no"
{...register("needInvoice")} // 注册"不开发票"
/>
不开发票
</label>
<label>
<input
type="radio"
value="yes"
{...register("needInvoice")} // 注册"开发票"
/>
开发票
</label>
{needInvoice === "yes" && ( // 当选择开发票时才显示下面字段
<div style={{ marginTop: 12 }}>
<div>
<input
placeholder="发票抬头"
{...register("invoiceTitle", {
required: "请输入发票抬头", // 仅显示时需要填写
})}
/>
{errors.invoiceTitle && <p>{errors.invoiceTitle.message}</p>} {/* 显示错误 */}
</div>
<div>
<input
placeholder="税号"
{...register("taxNo", {
required: "请输入税号", // 仅显示时需要填写
})}
/>
{errors.taxNo && <p>{errors.taxNo.message}</p>} {/* 显示错误 */}
</div>
</div>
)}
<button type="submit">提交订单</button>
</form>
);
}
export default App; // 导出组件
1) 坑点:
a.隐藏字段还在报错
你把字段隐藏了,但错误还在。
你可以理解为"虽然看不见,但表单状态里它还存在"。
解决思路:
切换时清空相关值 / 清除错误
或用 shouldUnregister: true(让卸载字段从表单中移除)
b.watch 过多导致页面频繁渲染
监听很多字段时,组件会更频繁刷新。
小项目问题不大,大表单要注意拆组件。
c.联动后忘记处理默认值
比如编辑页回填时,如果 needInvoice 是 yes,要确保发票字段也有默认值或回填值。
2)总结
_watch_就是"表单里的实时观察员":盯住某个值,一变就触发 UI 联动。
l.FormProvider / useFormContext(大表单拆组件必会)
jsx
import { useForm, FormProvider, useFormContext } from "react-hook-form"; // 引入 RHF 相关 API
interface FormData {
username: string;
email: string;
}
function UsernameField() { // 子组件:用户名输入
const {
register, // 从上下文拿 register
formState: { errors }, // 从上下文拿 errors
} = useFormContext<FormData>(); // 关键:不接收 props,直接拿父表单上下文
return (
<div>
<input
placeholder="请输入用户名"
{...register("username", {
required: "用户名必填", // 必填校验
minLength: { value: 2, message: "至少 2 个字符" }, // 最小长度校验
})}
/>
{errors.username && <p>{errors.username.message}</p>} {/* 显示错误 */}
</div>
);
}
function EmailField() { // 子组件:邮箱输入
const {
register, // 从上下文拿 register
formState: { errors }, // 从上下文拿 errors
} = useFormContext<FormData>(); // 使用上下文
return (
<div>
<input
placeholder="请输入邮箱"
{...register("email", {
required: "邮箱必填", // 必填
pattern: {
value: /^\S+@\S+\.\S+$/, // 简单邮箱正则
message: "邮箱格式不正确", // 格式错误提示
},
})}
/>
{errors.email && <p>{errors.email.message}</p>} {/* 显示错误 */}
</div>
);
}
export default function UseFormContext() { // 父组件
const methods = useForm<FormData>({
defaultValues: {
username: "", // 默认用户名
email: "", // 默认邮箱
},
mode: "onBlur", // 失焦校验
});
const onSubmit = (data: FormData) => { // 提交函数
alert(JSON.stringify(data, null, 2)); // 展示提交结果
};
return (
<FormProvider {...methods}> {/* 把整个表单方法"注入"上下文 */}
<form onSubmit={methods.handleSubmit(onSubmit)}> {/* 提交绑定 */}
<UsernameField /> {/* 子组件1 */}
<EmailField /> {/* 子组件2 */}
<button type="submit">提交</button> {/* 提交按钮 */}
</form>
</FormProvider>
);
}
1)3 个重点
a.FormProvider 放在表单外层
把 useForm() 的返回值 ...methods 传进去。
b.子组件里用 useFormContext() 拿能力
不再手动传 register/errors/control。
c.特别适合大表单拆模块
比如"基础信息模块""地址模块""发票模块"。
2)坑点
a.忘了包 FormProvider
子组件调用 useFormContext() 会拿不到值(直接报错)。
b.子组件里又 useForm() 一次
会变成"两个表单系统",数据不通。
子组件要用 useFormContext(),不要重复开新表单。
c.name 冲突
不同子组件注册了同名字段,值会互相覆盖。
d.深层组件错误提示取错路径
像 user.email 这种嵌套字段,要注意 errors.user?.email?.message。
3)必知
这里是必须学会FormProvider / useFormContext是如何使用的,因为在项目中不会用像之前前面的例子那样子使用,基本上都是分组件使用,然后配合Controller封装第三方组件来作为form的一员。这个例子的结构很经典,值得多次学习
m.Zod + resolver(把校验规则集中管理)
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
import { z } from "zod"; // 引入 zod
import { zodResolver } from "@hookform/resolvers/zod"; // 引入 zod 适配器
const schema = z.object({ // 定义整张表单的校验规则
username: z
.string()
.min(2, "用户名至少 2 个字符"),
email: z.email("邮箱格式不正确"), // Zod v4:email 已是独立 schema,不再挂在 .string() 上
age: z
.string()
.refine((v) => /^\d+$/.test(v), "年龄必须是整数") // 必须全是数字
.refine((v) => parseInt(v, 10) >= 18, "年龄不能小于 18"), // 最小 18
password: z
.string()
.min(6, "密码至少 6 位"),
confirmPassword: z
.string()
.min(6, "确认密码至少 6 位"),
}).refine((data) => data.password === data.confirmPassword, { // 跨字段校验
message: "两次密码不一致",
path: ["confirmPassword"],
});
// 直接用 z.infer 派生,输入输出类型一致,不再分叉
type FormData = z.infer<typeof schema>;
export default function App() { // 主组件
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: "onBlur",
defaultValues: {
username: "",
email: "",
age: "", // age 始终是字符串,无类型冲突
password: "",
confirmPassword: "",
},
});
const onSubmit = (data: FormData) => {
alert("提交成功:\n" + JSON.stringify(data, null, 2));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input placeholder="用户名" {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
</div>
<div>
<input placeholder="邮箱" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<input placeholder="年龄" {...register("age")} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<input type="password" placeholder="密码" {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
</div>
<div>
<input
type="password"
placeholder="确认密码"
{...register("confirmPassword")}
/>
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
</div>
<button type="submit">提交</button>
</form>
);
}
1)优点
a.把校验写在 register("xx", { required... }) 里,字段一多会很分散。\ 现在我们把规则"统一写在一个文件/一个对象里",表单只负责渲染。
b.规则集中管理:一眼看全表单规则
c.跨字段校验:比如"确认密码必须一致"
d.类型更清晰:尤其 TS 项目里很香(后面你可以再进阶)
n.实战表单(新增/编辑共用一个组件)
jsx
import { useEffect, useState } from "react"; // 引入 React 和 hooks
import { useForm } from "react-hook-form"; // 引入 RHF
import { z } from "zod"; // 引入 zod
import { zodResolver } from "@hookform/resolvers/zod"; // 引入 zod 解析器
interface User {
id: number;
name: string;
email: string;
role: "user" | "admin";
}
const userSchema = z.object({ // 定义表单校验规则
name: z.string().min(2, "姓名至少 2 个字符"), // name 校验
email: z.email("邮箱格式不正确"), // email 校验
role: z.enum(["user", "admin"], { message: "角色不合法" }), // role 必须是 user/admin
});
type UserFormValues = z.infer<typeof userSchema>; // 从 schema 推断表单字段类型(不含 id)
function UserForm({ initialData, onCreate, onUpdate }: { initialData: User | null, onCreate: (values: UserFormValues) => void, onUpdate: (id: number, values: UserFormValues) => void }) { // 共用表单组件
const isEditMode = !!initialData?.id; // 有 id 就是编辑模式
const {
register, // 注册字段
handleSubmit, // 提交处理
reset, // 重置/回填表单
formState: { errors, isSubmitting }, // 错误对象 + 提交中状态
} = useForm<UserFormValues>({
resolver: zodResolver(userSchema), // 接入 zod
defaultValues: {
name: "", // 默认 name
email: "", // 默认 email
role: "user", // 默认 role
},
});
useEffect(() => { // 监听 initialData 变化
if (initialData) { // 如果有初始数据(编辑)
reset({
name: initialData.name ?? "", // 回填 name
email: initialData.email ?? "", // 回填 email
role: initialData.role ?? "user", // 回填 role
});
} else { // 没有初始数据(新增)
reset({
name: "", // 清空 name
email: "", // 清空 email
role: "user", // 还原 role 默认值
});
}
}, [initialData, reset]); // 依赖:initialData 或 reset 变化时执行
const onSubmit = async (values: UserFormValues) => { // 提交函数
if (isEditMode) { // 编辑模式
await onUpdate(initialData.id, values); // 调用更新
} else { // 新增模式
await onCreate(values); // 调用创建
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<h3>{isEditMode ? "编辑用户" : "新增用户"}</h3> {/* 标题根据模式切换 */}
<div>
<input placeholder="姓名" {...register("name")} /> {/* 注册 name */}
{errors.name && <p>{errors.name.message}</p>} {/* 显示 name 错误 */}
</div>
<div>
<input placeholder="邮箱" {...register("email")} /> {/* 注册 email */}
{errors.email && <p>{errors.email.message}</p>} {/* 显示 email 错误 */}
</div>
<div>
<select {...register("role")}> {/* 注册 role */}
<option value="user">普通用户</option> {/* user 选项 */}
<option value="admin">管理员</option> {/* admin 选项 */}
</select>
{errors.role && <p>{errors.role.message}</p>} {/* 显示 role 错误 */}
</div>
<button type="submit" disabled={isSubmitting}> {/* 提交按钮,提交中禁用 */}
{isSubmitting ? "提交中..." : isEditMode ? "保存修改" : "创建用户"}
</button>
</form>
);
}
export default function Demo() { // 页面组件(模拟列表 + 选中编辑)
const [editingUser, setEditingUser] = useState<User | null>(null); // 当前编辑用户
const [users, setUsers] = useState([ // 模拟用户列表
{ id: 1, name: "张三", email: "zhangsan@test.com", role: "user" },
{ id: 2, name: "李四", email: "lisi@test.com", role: "admin" },
]);
const handleCreate = async (values: UserFormValues) => { // 创建逻辑
const newUser = { ...values, id: Date.now() }; // 生成新用户
setUsers((prev) => [newUser, ...prev]); // 插入列表
setEditingUser(null); // 清空编辑态(保持新增状态)
alert("创建成功"); // 提示
};
const handleUpdate = async (id: number, values: UserFormValues) => { // 更新逻辑
setUsers((prev) =>
prev.map((u) => (u.id === id ? { ...u, ...values } : u))
); // 按 id 更新用户
setEditingUser(null); // 保存后退出编辑态
alert("更新成功"); // 提示
};
return (
<div style={{ display: "grid", gap: 16 }}>
<UserForm
initialData={editingUser} // 传入当前编辑数据(为空则新增)
onCreate={handleCreate} // 传入创建函数
onUpdate={handleUpdate} // 传入更新函数
/>
<hr />
<h3>用户列表</h3>
{users.map((u) => (
<div key={u.id} style={{ display: "flex", gap: 8 }}>
<span>
{u.name} - {u.email} - {u.role}
</span>
<button onClick={() => setEditingUser(u as User)}>编辑</button> {/* 进入编辑 */}
<button onClick={() => setEditingUser(null)}>新增模式</button> {/* 切新增 */}
</div>
))}
</div>
);
}
1)4个关键点
a. initialData:是否有初始数据决定新增/编辑
b. reset(...):切换编辑对象时必须回填,不要只靠 defaultValues
c. isEditMode:!!initialData?.id 一把梭判断模式
d. onCreate/onUpdate 分离:提交逻辑更清楚,后期接接口方便
2)常见坑点
a.以为 defaultValues 会自动跟着 props 变
不会。defaultValues 只在初始化生效。
所以编辑切换时要 reset(newData)。
b.编辑 A 后切到新增,旧值还在
说明你没在 initialData 变空时 reset 成空值。
c.提交按钮重复点导致多次请求
用 isSubmitting 禁用按钮。
d.把 id 也放进表单字段里乱改
通常 id 不让用户编辑,更新时从 initialData.id 取。
o.封装可复用表单组件
抽成两个组件:
FormInput:文本输入框
FormSelect:下拉框
jsx
import React from "react"; // 引入 React
import { useForm, FormProvider, useFormContext } from "react-hook-form"; // 引入 RHF 相关 API
import { z } from "zod"; // 引入 zod
import { zodResolver } from "@hookform/resolvers/zod"; // 引入 zod resolver
const schema = z.object({ // 定义校验规则
name: z.string().min(2, "姓名至少 2 个字符"), // name 规则
email: z.string().email("邮箱格式不正确"), // email 规则
role: z.enum(["user", "admin"], { message: "角色不合法" }), // role 规则
});
function FormInput({ name, label, placeholder, type = "text" }) { // 通用输入框组件
const {
register, // 从上下文拿 register
formState: { errors }, // 从上下文拿 errors
} = useFormContext(); // 读取 FormProvider 提供的表单上下文
const errorMessage = errors?.[name]?.message; // 读取当前字段错误信息
return (
<div style={{ marginBottom: 12 }}> {/* 每个字段下方留点间距 */}
<label style={{ display: "block", marginBottom: 4 }}>{label}</label> {/* 字段标题 */}
<input
type={type} // 输入框类型
placeholder={placeholder} // 占位提示
{...register(name)} // 注册字段
/>
{errorMessage && <p style={{ color: "red" }}>{String(errorMessage)}</p>} {/* 显示错误 */}
</div>
);
}
function FormSelect({ name, label, options }) { // 通用下拉组件
const {
register, // 从上下文拿 register
formState: { errors }, // 从上下文拿 errors
} = useFormContext(); // 读取上下文
const errorMessage = errors?.[name]?.message; // 读取当前字段错误
return (
<div style={{ marginBottom: 12 }}> {/* 字段容器 */}
<label style={{ display: "block", marginBottom: 4 }}>{label}</label> {/* 字段标题 */}
<select {...register(name)}> {/* 注册字段 */}
{options.map((item) => (
<option key={item.value} value={item.value}> {/* 渲染选项 */}
{item.label}
</option>
))}
</select>
{errorMessage && <p style={{ color: "red" }}>{String(errorMessage)}</p>} {/* 显示错误 */}
</div>
);
}
export default function UserFormPage() { // 页面组件
const methods = useForm({
resolver: zodResolver(schema), // 接入 zod 校验
defaultValues: {
name: "", // 默认 name
email: "", // 默认 email
role: "user", // 默认 role
},
mode: "onBlur", // 失焦校验
});
const onSubmit = (values) => { // 提交成功回调
alert("提交成功:\n" + JSON.stringify(values, null, 2)); // 展示数据
};
return (
<FormProvider {...methods}> {/* 把整个表单方法通过上下文传下去 */}
<form onSubmit={methods.handleSubmit(onSubmit)}> {/* 绑定提交 */}
<h3>用户表单(封装版)</h3>
<FormInput
name="name" // 字段名
label="姓名" // 标签文本
placeholder="请输入姓名" // 占位符
/>
<FormInput
name="email" // 字段名
label="邮箱" // 标签文本
placeholder="请输入邮箱" // 占位符
type="email" // 输入类型
/>
<FormSelect
name="role" // 字段名
label="角色" // 标签文本
options={[
{ label: "普通用户", value: "user" }, // 选项1
{ label: "管理员", value: "admin" }, // 选项2
]}
/>
<button type="submit" disabled={methods.formState.isSubmitting}> {/* 提交按钮 */}
{methods.formState.isSubmitting ? "提交中..." : "提交"}
</button>
</form>
</FormProvider>
);
}
1)要点:
需要包FormProvider,子组件里使用 useFormContext()拿到数据
p.useController的基本使用
1)useController是什么?
你可以理解为:
Controller 是"组件写法"(JSX 包一层)
useController 是"Hook 写法"(先拿到 field,再自己随便渲染)
两者本质一样,只是写法不同。
2)对比下:
直接用 Controller
jsx
import React from "react"; // 引入 React
import { useForm, Controller } from "react-hook-form"; // 引入 useForm 和 Controller
function MySwitch({ value, onChange }) { // 自定义开关组件
return (
<button type="button" onClick={() => onChange(!value)}> {/* 点击切换 */}
{value ? "开" : "关"} {/* 根据 value 显示 */}
</button>
);
}
export default function DemoController() { // 页面组件
const { control, handleSubmit } = useForm({
defaultValues: { agree: false }, // 默认值
});
const onSubmit = (data) => console.log(data); // 提交回调
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<Controller
name="agree" // 字段名
control={control} // 表单控制器
rules={{ required: "请先同意" }} // 校验规则
render={({ field, fieldState }) => ( // 在 render 里拿到 field
<div>
<p>是否同意</p>
<MySwitch
value={field.value} // 当前值
onChange={field.onChange} // 值变化回传
/>
{fieldState.error && <p style={{ color: "red" }}>{fieldState.error.message}</p>} {/* 错误 */}
</div>
)}
/>
<button type="submit">提交</button>
</form>
);
}
使用 useController
jsx
import React from "react"; // 引入 React
import { useForm, useController } from "react-hook-form"; // 引入 useForm 和 useController
function MySwitch({ value, onChange }) { // 自定义开关组件
return (
<button type="button" onClick={() => onChange(!value)}> {/* 点击切换 */}
{value ? "开" : "关"} {/* 根据 value 显示 */}
</button>
);
}
function AgreeField({ control }) { // 单独封装字段组件
const { field, fieldState } = useController({
name: "agree", // 字段名
control, // 表单控制器
rules: { required: "请先同意" }, // 校验规则
defaultValue: false, // 默认值(建议写上)
});
return (
<div>
<p>是否同意</p>
<MySwitch
value={field.value} // 当前值
onChange={field.onChange} // 值变化回传
/>
{fieldState.error && <p style={{ color: "red" }}>{fieldState.error.message}</p>} {/* 错误 */}
</div>
);
}
export default function DemoUseController() { // 页面组件
const { control, handleSubmit } = useForm({
defaultValues: { agree: false }, // 默认值
});
const onSubmit = (data) => console.log(data); // 提交回调
return (
<form onSubmit={handleSubmit(onSubmit)}> {/* 绑定提交 */}
<AgreeField control={control} /> {/* 使用封装字段 */}
<button type="submit">提交</button>
</form>
);
}
3)差异:
Controller:\ 你在 JSX 里写<Controller render={...} />,\ field 在 render 里面拿。useController:\ 你在组件顶部const { field } = useController(...),\ field 先拿出来,再自由渲染 JSX。
4)使用场景:
- 页面里就一两个特殊组件,图省事:
**Controller** - 要封装通用组件(
FormSwitch/FormDatePicker):**useController**
q.使用useController封装通用组件
1)封装FormSwitch.tsx
jsx
import {
Control, // 表单 control 类型
FieldValues, // 表单数据的通用约束类型
Path, // 字段路径类型(保证 name 合法)
PathValue, // 字段路径对应的值类型
RegisterOptions, // rules 类型
useController, // 核心 hook
} from "react-hook-form"; // 引入 RHF 类型和 hook
type FormSwitchProps<T extends FieldValues> = {
control: Control<T>; // 来自 useForm 的 control,且和表单 T 绑定
name: Path<T>; // 字段名,受 T 约束
label?: string; // 标签(可选)
defaultValue?: boolean; // 默认值(布尔)
rules?: RegisterOptions<T, Path<T>>; // 校验规则
disabled?: boolean; // 是否禁用
};
export function FormSwitch<T extends FieldValues>({
control,
name,
label,
defaultValue = false,
rules,
disabled = false,
}: FormSwitchProps<T>) {
const { field, fieldState } = useController({
control, // 绑定表单 control
name, // 绑定字段名
defaultValue: defaultValue as PathValue<T, Path<T>>, // 给默认值(PathValue 保证类型安全)
rules, // 绑定校验规则
});
return (
<div style={{ marginBottom: 12 }}>
{label && <label>{label}</label>}
<br />
<button
type="button" // 避免触发表单提交
disabled={disabled}
onClick={() => field.onChange(!field.value)} // 切换 true/false
onBlur={field.onBlur} // 通知 RHF:字段失焦
style={{
padding: "6px 12px",
background: field.value ? "green" : "gray",
color: "#fff",
border: "none",
borderRadius: 6,
cursor: disabled ? "not-allowed" : "pointer",
}}
>
{field.value ? "已开启" : "已关闭"}
</button>
{fieldState.error && (
<p style={{ color: "red", margin: "6px 0 0" }}>
{fieldState.error.message}
</p>
)}
</div>
);
}
2)封装FormDatePicker.tsx
jsx
type FormDatePickerProps<T extends FieldValues> = {
control: Control<T>; // useForm 返回的 control
name: Path<T>; // 字段名(类型安全)
label?: string; // 标签
defaultValue?: string; // 默认值(yyyy-mm-dd)
rules?: RegisterOptions<T, Path<T>>; // 校验规则
disabled?: boolean; // 是否禁用
};
export function FormDatePicker<T extends FieldValues>({
control,
name,
label,
defaultValue = "",
rules,
disabled = false,
}: FormDatePickerProps<T>) {
const { field, fieldState } = useController({
control, // 绑定 control
name, // 绑定字段
defaultValue: defaultValue as PathValue<T, Path<T>>, // 默认值(PathValue 保证类型安全)
rules, // 校验规则
});
return (
<div style={{ marginBottom: 12 }}>
{label && <label>{label}</label>}
<br />
<input
type="date" // 原生日期选择
value={(field.value ?? "") as string} // 防止 undefined
onChange={(e) => field.onChange(e.target.value)} // 把字符串传回 RHF
onBlur={field.onBlur} // 失焦同步
disabled={disabled}
/>
{fieldState.error && (
<p style={{ color: "red", margin: "6px 0 0" }}>
{fieldState.error.message}
</p>
)}
</div>
);
}
3)使用
jsx
import { useForm } from "react-hook-form"; // 引入 useForm
type FormValues = {
enabled: boolean; // 开关字段
birthday: string; // 日期字段(yyyy-mm-dd)
};
export default function DemoForm() {
const { control, handleSubmit } = useForm<FormValues>({
defaultValues: {
enabled: false, // 默认关闭
birthday: "", // 默认空日期
},
mode: "onBlur",
});
const onSubmit = (data: FormValues) => {
console.log("提交数据:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h3>TS 通用组件示例</h3>
<FormSwitch<FormValues>
control={control}
name="enabled"
label="是否启用"
rules={{ required: "请确认是否启用" }}
/>
<FormDatePicker<FormValues>
control={control}
name="birthday"
label="生日"
rules={{ required: "请选择生日" }}
/>
<button type="submit">提交</button>
</form>
);
}
r.使用FormProvider + useFormContext一起封装组件
1)"旧方式"的痛点
旧方式你会这样写:
jsx
<FormSwitch control={control} name="enabled" />
<FormDatePicker control={control} name="birthday" />
组件越多,你就越要不停传 control。\ 如果中间隔了 2~3 层子组件,会更麻烦(这叫"props drilling",你可以理解为"接力传参")。
2)新方式核心思路(就两步)
第 1 步:在最外层包上 FormProvider
把 useForm() 拿到的方法整体塞进去。
第 2 步:在子组件里用 useFormContext()
子组件自己拿 control,不再让父组件传。
3)封装
FormSwitch.tsx
jsx
import {
FieldValues, // 表单值类型约束
Path, // name 的路径类型
PathValue, // 字段路径对应的值类型
useController, // 绑定字段
useFormContext, // 从 Provider 拿表单上下文
} from "react-hook-form"; // 引入 RHF
type FormSwitchProps<T extends FieldValues> = {
name: Path<T>; // 字段名
label?: string; // 标签
};
export function FormSwitch<T extends FieldValues>({
name,
label,
}: FormSwitchProps<T>) {
const { control } = useFormContext<T>(); // 直接从上下文拿 control(关键)
const { field } = useController({
control, // 绑定 control
name, // 绑定字段
defaultValue: false as PathValue<T, Path<T>>, // 默认值(PathValue 保证类型安全)
});
return (
<div style={{ marginBottom: 12 }}>
{label && <label>{label}</label>}
<br />
<button
type="button" // 避免触发表单提交
onClick={() => field.onChange(!field.value)} // 切换 true/false
>
{field.value ? "已开启" : "已关闭"}
</button>
</div>
);
}
FormDatePicker.tsx
jsx
type FormDatePickerProps<T extends FieldValues> = {
name: Path<T>; // 字段名
label?: string; // 标签
};
export function FormDatePicker<T extends FieldValues>({
name,
label,
}: FormDatePickerProps<T>) {
const { control } = useFormContext<T>(); // 直接拿 control(关键)
const { field } = useController({
control, // 绑定 control
name, // 绑定字段
defaultValue: "" as PathValue<T, Path<T>>, // 默认值(PathValue 保证类型安全)
});
return (
<div style={{ marginBottom: 12 }}>
{label && <label>{label}</label>}
<br />
<input
type="date" // 日期输入
value={(field.value ?? "") as string} // 防止 undefined
onChange={(e) => field.onChange(e.target.value)} // 回填表单
/>
</div>
);
}
使用:
jsx
import { FormProvider, useForm } from "react-hook-form"; // 引入 RHF
type FormValues = {
enabled: boolean; // 开关值
birthday: string; // 日期值
};
export default function DemoForm() {
const methods = useForm<FormValues>({
defaultValues: {
enabled: false, // 默认关闭
birthday: "", // 默认空
},
});
const onSubmit = (data: FormValues) => {
console.log("提交数据:", data); // 查看提交结果
};
return (
<FormProvider {...methods}>
{/* 把整个表单能力放进上下文 */}
<form onSubmit={methods.handleSubmit(onSubmit)}>
<h3>FormProvider + useFormContext 示例</h3>
{/* 注意:这里不再传 control 了 */}
<FormSwitch<FormValues> name="enabled" label="是否启用" />
<FormDatePicker<FormValues> name="birthday" label="生日" />
<button type="submit">提交</button>
</form>
</FormProvider>
);
}
4)总结:
FormProvider 负责"提供",useFormContext 负责"取用"。
s.AntD 对接 React Hook Form
1)封装预期:
FormSwitch.tsx(封装 AntD Switch)FormSelect.tsx(封装 AntD Select)FormDatePicker.tsx(封装 AntD DatePicker)UserForm.tsx(页面:FormProvider + 提交转换)
2)封装
jsx
import { Switch } from "antd"; // 引入 AntD 的 Switch
import { FieldValues, Path, useController, useFormContext } from "react-hook-form"; // 引入 RHF 相关能力
type FormSwitchProps<T extends FieldValues> = {
name: Path<T>; // 字段名(受 TS 约束)
label?: string; // 标签文案
};
export function FormSwitch<T extends FieldValues>({ name, label }: FormSwitchProps<T>) {
const { control } = useFormContext<T>(); // 从 FormProvider 上下文中拿 control
const { field, fieldState } = useController({
name, // 绑定字段名
control, // 绑定 control
});
return (
<div style={{ marginBottom: 16 }}>
{label && <div style={{ marginBottom: 8 }}>{label}</div>}
<Switch
checked={!!field.value} // Switch 用 checked,不是 value
onChange={(checked) => field.onChange(checked)} // onChange 直接给 boolean
/>
{fieldState.error && (
<div style={{ color: "red", marginTop: 6 }}>{fieldState.error.message}</div>
)}
</div>
);
}
jsx
import { Select } from "antd"; // 引入 AntD Select
type Option = {
label: string; // 选项显示文本
value: string; // 选项值
};
type FormSelectProps<T extends FieldValues> = {
name: Path<T>; // 字段名
label?: string; // 标签
options: Option[]; // 选项列表
mode?: "multiple"; // 是否多选
placeholder?: string; // 占位符
};
export function FormSelect<T extends FieldValues>({
name,
label,
options,
mode,
placeholder,
}: FormSelectProps<T>) {
const { control } = useFormContext<T>(); // 从上下文拿 control
const { field, fieldState } = useController({
name, // 字段名
control, // control
});
return (
<div style={{ marginBottom: 16 }}>
{label && <div style={{ marginBottom: 8 }}>{label}</div>}
<Select
style={{ width: 320 }} // 给个宽度
mode={mode} // 多选模式
options={options} // 选项
value={field.value} // 当前值(单选 string / 多选 string[])
onChange={(val) => field.onChange(val)} // Select onChange 直接给值
onBlur={field.onBlur} // blur
placeholder={placeholder} // 占位
/>
{fieldState.error && (
<div style={{ color: "red", marginTop: 6 }}>{fieldState.error.message}</div>
)}
</div>
);
}
jsx
import { DatePicker } from "antd"; // 引入 AntD DatePicker
import dayjs, { Dayjs } from "dayjs"; // 引入 dayjs
type FormDatePickerProps<T extends FieldValues> = {
name: Path<T>; // 字段名
label?: string; // 标签
placeholder?: string; // 占位符
format?: string; // 日期格式
};
export function FormDatePicker<T extends FieldValues>({
name,
label,
placeholder,
format = "YYYY-MM-DD",
}: FormDatePickerProps<T>) {
const { control } = useFormContext<T>(); // 从上下文拿 control
const { field, fieldState } = useController({
name, // 字段名
control, // control
});
return (
<div style={{ marginBottom: 16 }}>
{label && <div style={{ marginBottom: 8 }}>{label}</div>}
<DatePicker
format={format} // 显示格式
placeholder={placeholder} // 占位
value={field.value ? dayjs(field.value, format) : null} // RHF 里存 string,这里转成 dayjs
onChange={(date: Dayjs | null) => {
field.onChange(date ? date.format(format) : ""); // 选中后再转回 string 存入表单
}}
onBlur={field.onBlur} // blur
/>
{fieldState.error && (
<div style={{ color: "red", marginTop: 6 }}>{fieldState.error.message}</div>
)}
</div>
);
}
jsx
import { Button } from "antd"; // 引入 AntD Button
import { FormProvider, useForm } from "react-hook-form"; // 引入 RHF
type UserFormValues = {
enabled: boolean; // 开关
hobbies: string[]; // 多选爱好
birthday: string; // 生日(字符串,便于传后端)
};
export default function UserForm() {
const methods = useForm<UserFormValues>({
defaultValues: {
enabled: true, // 默认开启
hobbies: [], // 默认空数组
birthday: "", // 默认空字符串
},
mode: "onBlur", // 失焦触发校验(示例配置)
});
const onSubmit = (values: UserFormValues) => {
// 这里演示"提交前统一转换"
const payload = {
...values, // 先展开原值
enabled: values.enabled ? 1 : 0, // 比如后端要 1/0
birthday: values.birthday || null, // 空字符串转 null
};
console.log("表单原始值:", values); // 看 RHF 内部值
console.log("提交给后端:", payload); // 看最终提交值
};
return (
<FormProvider {...methods}>
{/* 提供表单上下文,子组件可 useFormContext 获取 control */}
<form onSubmit={methods.handleSubmit(onSubmit)} style={{ padding: 24 }}>
<h2>AntD + RHF 示例</h2>
<FormSwitch<UserFormValues> name="enabled" label="是否启用" />
<FormSelect<UserFormValues>
name="hobbies"
label="爱好(可多选)"
mode="multiple"
placeholder="请选择爱好"
options={[
{ label: "篮球", value: "basketball" },
{ label: "音乐", value: "music" },
{ label: "旅行", value: "travel" },
]}
/>
<FormDatePicker<UserFormValues>
name="birthday"
label="生日"
placeholder="请选择生日"
format="YYYY-MM-DD"
/>
<Button type="primary" htmlType="submit">
提交
</Button>
</form>
</FormProvider>
);
}
3)坑点:
- Switch 用错属性
- 错误:
value={field.value} - 正确:
checked={field.value}
- 错误:
- DatePicker 值类型不匹配
- AntD 要
dayjs | null - 你表单里常存
string - 所以要"来回转换"
- AntD 要
- 默认值漏写
- 多选
Select不给[],容易从受控变非受控警告
- 多选
- 忘记 FormProvider
useFormContext会直接报错
- 提交前不做格式转换
- 前端内部值适合交互,不一定是后端最终想要的格式
t.项目实际使用:HOC转换,封装通用组件(基础理解版)
1)封装FormContainer
jsx
import { useRef } from "react";
import { useForm,FormProvider,UseFormReturn } from "react-hook-form";
/**
* 创建一个 ref,用于在组件外部访问 form 实例(如手动调用 reset、setValue 等方法)。
* 泛型 Values 约束表单数据的类型结构,默认为任意键值对。
*/
export const useFormRef = <
Values extends Record<string, any> = Record<string, any>,
>() => {
return useRef<UseFormReturn<Values> | null>(null);
};
interface FormProps {
defaultValues: any;
/** 表单校验通过后的提交回调 */
onFinish: (data: any) => void;
/** 表单校验失败后的回调(可选) */
onFinishFailed?: (errors: any) => void;
children: React.ReactNode;
/** 外部 ref,挂载后可通过 formRef.current 调用 react-hook-form 的所有方法 */
formRef?: React.RefObject<UseFormReturn<any> | null>;
}
export function Form(props: FormProps) {
const {defaultValues,onFinish,onFinishFailed,children,formRef} = props;
// useForm 是 react-hook-form 的核心 hook,返回表单控制对象(包含 register、control、handleSubmit 等)
const form = useForm({defaultValues});
// 将 form 实例同步到外部 ref,使父组件可以直接调用 form 方法(如 reset)
if(formRef){
formRef.current = form;
}
// FormProvider 通过 React Context 将 form 实例共享给所有子组件
// 子组件可通过 useFormContext() 读取,无需层层传 prop
return <FormProvider {...form}>
{/* handleSubmit 会先执行校验,通过后调用 onFinish,失败则调用 onFinishFailed */}
<form onSubmit={form.handleSubmit(onFinish,onFinishFailed)}>
{children}
</form>
</FormProvider>
}
2)封装FormItem样式
jsx
/**
* FormItem ------ 纯 UI 展示组件,负责渲染表单项的三层结构:
* 1. 标签行(label + 必填星号)
* 2. 输入控件(children,由外部传入)
* 3. 错误提示(来自 react-hook-form 的校验信息)
*
* 它本身不感知 react-hook-form,只是一个"外壳"容器。
*/
export interface FormItemProps {
label?: React.ReactNode
required?: boolean
/** 校验失败时的错误文案,由 WithFormItem 从 fieldState.error.message 注入 */
error?: string
children: React.ReactNode
}
export function FormItem({label,required,error,children}: FormItemProps) {
return (
<div>
{/* 标签行:required 为 true 时显示红色星号提示必填 */}
<div>{label}{required ? <span>*</span> : null}</div>
{/* 实际的输入控件插槽 */}
<div>{children}</div>
{/* 错误信息行:error 有值时才渲染,避免占用空白空间 */}
<div style={{ color: 'red' }}>{error ? <span>{error}</span> : null}</div>
</div>
)
}
3)封装withFormItem HOC高阶组件
jsx
import { FormItem, type FormItemProps } from "./FormItem";
import { useController, useFormContext} from 'react-hook-form'
/** react-hook-form 字段必须具备的 props */
interface RHFFieldProps {
/** 字段在表单数据对象中的 key,对应 defaultValues 的属性名 */
name: string;
/** 校验规则,同 react-hook-form 的 RegisterOptions(required / minLength / pattern 等) */
rules?: Record<string, any>;
defaultValue?: unknown;
}
/**
* WithFormItem ------ 高阶组件(HOC)
*
* 作用:把任意 UI 输入组件(如 antd 的 Input、Select)接入 react-hook-form 的状态管理体系。
*
* 工作流程:
* 1. 通过 useFormContext() 从 FormProvider 提供的 Context 中取出 control 对象。
* 2. 通过 useController() 用 name + control + rules 注册字段,
* 返回 field(受控绑定)和 fieldState(校验状态)。
* 3. 将 field.value / onChange / onBlur 注入被包裹的 Component,使其变为受控组件。
* 4. 将 fieldState.error.message 传给 FormItem,用于显示错误提示。
*
* 泛型 P:被包裹组件自身的 props 类型,确保透传时类型安全。
* 最终组件的 props = 组件原有 props(P) + FormItem 展示 props + RHF 字段 props
*/
export function WithFormItem<P extends Record<string, any>>(Component: React.ComponentType<P>) {
return function New(props: P & Omit<FormItemProps, 'children' | 'error'> & RHFFieldProps) {
// 从最近的 FormProvider Context 中取出 control,无需手动传递
const {control} = useFormContext()
// 拆分出 RHF 专属 props,剩余的 rest 会透传给 FormItem(如 label、required、style 等)
const {name,rules,defaultValue,...rest} = props
// useController 将字段注册到 react-hook-form,
// field ------ 包含 value / onChange / onBlur / ref,用于绑定到输入控件
// fieldState ------ 包含 error / isDirty / isTouched 等状态
const {field,fieldState} = useController({
name,
control,
rules,
defaultValue
})
return (
<div>
{/* error 来自 RHF 校验结果,label/required 等来自 rest */}
<FormItem error={fieldState.error?.message} {...rest}>
{/* 用 field 的受控属性覆盖组件原有的 value/onChange/onBlur,
使组件的输入变化能同步到 react-hook-form 的内部状态 */}
<Component {...props} value={field.value} onChange={field.onChange} onBlur={field.onBlur} />
</FormItem>
</div>
);
};
}
4)使用withFormItem进行包装第三方ui组件
jsx
import { Input, Select, DatePicker, Switch} from 'antd';
// import dayjs, { type Dayjs } from 'dayjs';
import { WithFormItem } from './WithFormItem';
/**
* 使用 WithFormItem HOC 将 antd 原生组件包裹为 react-hook-form 受控字段。
* 包裹后的组件同时具备:
* - antd 组件原有的所有 props(如 placeholder、size、options 等)
* - FormItem 的展示 props(label、required)
* - RHF 的字段 props(name、rules、defaultValue)
*/
export const RHFInput = WithFormItem(Input);
export const RHFSelect = WithFormItem(Select);
export const RHFDatePicker = WithFormItem(DatePicker);
export const RHFSwitch = WithFormItem(Switch);
5)使用示例
jsx
'use client';
import { Button } from 'antd';
import { Form, useFormRef } from './Form';
import { RHFInput, RHFSelect, RHFDatePicker,RHFSwitch } from './Fields';
/**
* DemoTest ------ 整个表单体系的使用示例
*
* 组件层次关系:
* Form (提供 FormProvider Context)
* └─ RHFInput / RHFSelect / ... (WithFormItem 包裹后的受控字段)
* ├─ FormItem (渲染 label + 错误信息)
* └─ antd 原生组件 (受 field.value/onChange 驱动)
*/
export function DemoTest(){
const onFinish = (data: any) => {
// 校验通过后,data 包含所有字段的最新值
console.log(data)
}
// useFormRef 返回一个 ref,Form 会在渲染时将 form 实例写入 ref.current
// 从而在组件外部(如按钮回调)也能调用 reset / setValue / getValues 等方法
const formRef = useFormRef();
const cancelFunction = () => {
console.log('formRef.current',formRef.current);
// 通过 ref 直接调用 react-hook-form 的 reset,将所有字段恢复为 defaultValues
formRef.current?.reset();
}
return (
// defaultValues 为各字段的初始值,onFinish 为提交成功回调
<Form formRef={formRef} defaultValues={{ username: '123', hobby: 'basketball', birthday: '', switch: true }} onFinish={onFinish}>
{/* name 对应 defaultValues 的 key;rules 为校验规则,失败时错误信息显示在字段下方 */}
<RHFInput name="username" label="用户名" rules={{ required: '请输入用户名' }} value="123"/>
<RHFSelect name="hobby" label="爱好" rules={{ required: '请选择爱好' }} value="basketball"
options={[
{ label: '篮球', value: 'basketball' },
{ label: '足球', value: 'football' },
{ label: '排球', value: 'volleyball' },
]}
style={{ width: '100px' }}
/>
<RHFDatePicker name="birthday" label="生日" rules={{ required: '请选择生日' }} />
<RHFSwitch name="switch" label="开关" checked={true}/>
{/* type="default" 普通按钮,手动触发重置 */}
<Button type="default" onClick={cancelFunction}>取消</Button>
{/* htmlType="submit" 触发原生 form 提交,进而执行 handleSubmit → onFinish */}
<Button type="primary" htmlType="submit">提交</Button>
</Form>
)
}