背景说明

nextjs 使用中不可避免地会碰到使用表单,尤其是登录表单。而对于提交表单来说要关注的几个要素就是数据收集、数据校验和数据提交(涉及异步操作)。本文将围绕这几个要素,并通过zodreact-hook-form提供的能力实现登录表单。

本文所述依赖如下的库包及其版本

包名 版本号
next 14.2.15
react 18.2.0
react-dom 18.2.0
zod 3.24.1
react-hook-form 7.54.2
@hookform/resolvers 3.9.1

本文的开发环境基于 Macbook Pro M1 MacOS 14.6.1。

zod 简单上手

zod 提供了简单的表单校验器,且支持typescript,并弥补了typescrip对运行时代码无法校验的不足,通过一次定义,不仅可以通过safeParse方法验证表单数据,导出错误,还可以通过z.infer推断表单模型的typescript类型。
下载

1
pnpm add zod

以邮箱登录表单为例

1
2
3
4
5
6
export const loginSchema = z.object({
email: z.string().email("邮箱地址格式错误").min(1, "邮箱地址不能为空"),
password: z.string().min(1, "密码不能为空")
})

export type LoginData = z.infer<typeof loginSchema>

image.png

image.png

image.png

可以看到,我们可以使用z.object()定义模型,通过schema.safeParse()方法返回的对象resp
通过resp.success可以判断数据是否校验成功,一旦校验成功则通过resp.data获取输入的表单数据,否则可以使用resp.error获取校验错误。

登录表单的UI实现

由于css样式比较繁琐,这里使用shadcn ui这个组件库+Tailwindcss。其中有邮箱和密码的输入框还有一个用于提交表单的按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default function LoginForm() {
return (
<form>
<div>
<Label htmlFor="login-email">邮箱:</Label>
<Input
id="login-email"
type="text"
autoComplete="off"
placeholder="请输入您的邮箱地址"
className={cn(
"border",
"focus-visible:!outline-none focus-visible:ring-1 focus-visible:ring-gray-500"
)}
/>
</div>
<div>
<Label htmlFor="login-pwd">密码:</Label>
<Input
id="login-pwd"
type="password"
autoComplete="new-password"
placeholder="请输入密码:"
className={cn(
"border",
"focus-visible:!outline-none focus-visible:ring-1 focus-visible:ring-gray-500"
)}
/>
</div>
<Button className="w-full">登录</Button>
</form>
);
}

image.png

使用 react-hook-form

基本使用

react-hook-form 提供了许多用于表单的hook,在这使用的是useForm这个hook。
基本用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {useForm} from "react-hook-form"
export default function Page(){
const {
handleSubmit,
formState: { errors, isSubmitting },
reset,
register,
} = useForm();
return <form onSubmit={handleSubmit((data)=>{
console.log("data",data)
})}>
<Label htmlFor="login-email">邮箱:</Label>
<Input
id="login-email"
type="text"
autoComplete="off"
placeholder="请输入您的邮箱地址"
{...register("email")}
className={cn(
"border",
"focus-visible:!outline-none focus-visible:ring-1 focus-visible:ring-gray-500"
)}
/>
</form>
}

可以看到useForm导出了三个方法和一个对象,其中最为重要的当属handleSubmitregister方法,一个用来注册某些原生属性或者事件以及校验规则。对于校验规则,其提供了一个resolver属性允许使用第三方的表单校验库去完成。

1
2
3
4
5
6
7
8
import { zodResolver } from "@hookform/resolvers/zod";
...
const {
handleSubmit,
formState: { errors, isSubmitting },
reset,
register,
} = useForm({ resolver: zodResolver(loginSchema) });

对此,我们可以使用上节提到的zod来实现表单校验,通过安装@hookform/resolvers导入zodResolver,传入上节定义号的表单校验模型。

ts智能提示

再通过useForm传入zod推断的数据类型,可以在使用register方法时得到更好的智能提示。

1
2
type LoginData = z.infer<typeof loginSchema>
useForm<LoginData>()

image.png

image.png

获取错误并修改表单样式

当表单校验发生错误时,常见的UI提示是输入框边框转为警告色,并在其下方显示对应的错误信息,就像下述这样,当邮箱地址格式错误显示错误的信息:

image.png
该错误信息在使用zod时已定义:

image.png

警告色样式可由tailwindcss提供:

image.png
但如何知道字段校验错误,并取出这些错误提示信息呢。答案是上文导出的errors属性。

1
const {formState:{errors}} = useForm()

导出errors属性,其也是一个对象,可通过errors.[field].message取出错误信息

1
console.log(errors.email.message) \\邮箱地址格式错误

可以使用一个p标签显示信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form>
<Label htmlFor="login-email">邮箱:</Label>
<Input
id="login-email"
type="text"
{...register("email")}
autoComplete="off"
placeholder="请输入您的邮箱地址"
className={cn(
"border",
"focus-visible:!outline-none focus-visible:ring-1 focus-visible:ring-gray-500",
errors.email
? "border-red-500 focus-visible:!ring-red-700"
: "border-gray-300 focus-visible:ring-gray-500"
)}
/>
{errors.email && (
<p className="text-red-500 text-sm">{errors.email.message}</p>
)}
</form>

至此,实现了字段校验错误信息的显示和UI的警告色功能。

表单提交时按钮禁用

当表单在提交时往往是异步状态,在提交结束前我们往往期望暂时禁用提交按钮,直到本次提交完成(无论成功与否)。

image.png

在当前的示例中,点击登录按钮,按钮立刻处于禁用样式其文字页变更为【登录中…】,很容易想到需要一个布尔值在表单提交时设置为true添加到buttondisabled属性和实现添加渲染不同的按钮文字。
幸运地是,useForm提供了这个属性,我们可以像得到errors对象属性一样,拿到这个布尔属性isSubmitting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {
handleSubmit,
formState: { errors, isSubmitting },
reset,
register,
} = useForm<LoginData>({ resolver: zodResolver(loginSchema) });

return (
...
<Button className="w-full" disabled={isSubmitting}>
{isSubmitting ? "登录中..." : "登录"}
</Button>
...
)

到这,按钮提交时禁用也实现了。

当本示例的登录表单完成校验后将会触发form元素的提交事件,执行在handleSubmit中传入的回调函数:

image.png

本文小结

本文通过登录表单的案例,介绍了如何使用zodreact-hook-form实现表单数据收集和校验的全过程和数据校验,并补充了一些需要的注意事项。