如何在next.js中处理表单提交

在前端的开发中,与后端交互必然是绕不开的话题,其中表单的处理则是占比很大的一部分,这篇文章就来探讨一下如何在next.js中处理表单提交。

准备

在探讨开始,我们需要通过next.js脚手架来创建一个空项目,打开终端输入:

shell 复制代码
npx create-next-app@latest form-demo

至于创建时的选项,按照个人习惯即可。对表单进行处理时,我们应该分为两个情况来进行。

第一种:简单表单的处理,对于简单的表单,我们可自行进行判断即可完成;

第二种:较复杂表单的处理,对于这类表单,一般来说依赖第三方库来处理更佳,也是重点部分。

简单表单

简单的表单处理十分简单...额,好像是废话 ̄□ ̄||...先看下方这种登录表单:

这种表单很简单,就两个input输入框,这种处理也十分简单:

  1. 我们可以在inputblur或者change事件后进行判断,然后在下方进行错误提示的回显处理;
  2. 在提交按钮的处理时间中进行判断,然后进行回显或者toast处理。

例如,下面通过提交事件进行toast来提醒用户,如下所示:

tsx 复制代码
'use client';
import { FormEvent, useState } from "react";
import { toast } from "react-toastify";

export default function Home() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (!username.trim() || !password.trim()) {
      toast('请输入用户名或者密码!');
      return;
    }
  }

  return (
    <div className="w-screen h-screen flex justify-center items-center">
      <div className="border border-gray-200 rounded-md shadow-md p-8">
        <h1 className="font-bold text-xl text-center">登录</h1>
        <form
          className="flex flex-col gap-y-4 justify-center items-center mt-6"
          onSubmit={onSubmit}
        >
          <div className="flex items-center gap-x-2">
            <p className="w-20 text-right">用户名:</p>
            <input
              className="w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm"
              placeholder="请输入用户名"
              value={username}
              onChange={e => setUsername(e.currentTarget.value)}
            />
          </div>
          <div className="flex items-center gap-x-2">
            <p className="w-20 text-right">密码:</p>
            <input
              className="w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm"
              placeholder="请输入密码"
              type="password"
              value={password}
              onChange={e => setPassword(e.currentTarget.value)}
            />
          </div>
          <div className="w-full border-t pt-4 mt-4 border-gray-200 text-right">
            <button
              type="submit"
              className="bg-black/90 text-white px-6 py-2 rounded-md text-sm cursor-pointer hover:shadow-md shadow-black/50"
            >登录</button>
          </div>
        </form>
      </div>
    </div>
  );
}

这种简单表单的处理方式很简单,也很灵活。没有太多讨论的必要,下面我们来讨论一下较为复杂的表单处理。

复杂表单

我们先来看一下复杂的表单,如下图所示:

像这种表单,包含了基本的input输入框,也包含了选择框和开关、日期、文件上传等form组件,甚至像友情链接这种字段,还会加入动态的json数组。

对于这类表单,如果还自己去判断的话,尽管灵活可自定义,但是却有很冗余的代码,以及维护其他没那么容易。所以,对于这类表单,最好接住第三方库。

react-hook-form

react-hook-form是基于react的一款表单验证处理方案,所以同样也适用于next.js框架,通过如下命令进行依赖安装:

shell 复制代码
npm i react-hook-form

如何使用该库呢?如下代码所示:

tsx 复制代码
export default function Home() {
    const {
        register,
        watch,
        handleSubmit,
        formState: { errors }
    } = useForm();
    return (...);
}

通过useForm钩子函数,我们可以获得结构的如:register,watch,handleSubmit三个方法,以及一个errors对象,当然useForm不止这些参数。

对于以上解构出来的参数,其意义分别如下:

  • register 注册对应的form表单值,表单值、验证、状态都由react-hook-form来管理;
  • watch 监听对应表单值的变化;
  • handleSubmit 包装表单的提交函数;
  • errors 表单的错误信息(验证不通过的信息将在该对象里)

register/errors/handleSubmit

现在,我们来尝试使用,修改代码如下:

tsx 复制代码
'use client';

import { SubmitHandler, useForm } from "react-hook-form";

type FormType = {
  title: string;
  description: string;
}
export default function Home() {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors }
  } = useForm({
    defaultValues: {
      title: '',
      description: ''
    }
  });

  const onSubmit: SubmitHandler<FormType> = (data) => {
    console.log('form data:', data);
  }

  return (
    <div className="w-screen h-screen flex justify-center items-center">
      <div className="border border-gray-200 rounded-md shadow-md p-8">
        <h1 className="font-bold text-xl text-center">网站设置</h1>
        <form className="mt-8" onSubmit={handleSubmit(onSubmit)}>
          <div className="w-4xl grid grid-cols-2 gap-4">
            <div>
              <div className="flex items-center gap-x-2">
                <p className="w-20 text-right">网站标题:</p>
                <input
                  className="w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm"
                  placeholder="请输入标题"
                  {...register('title', { required: '请输入网站标题' })}
                />
              </div>
              <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.title?.message}</p>
            </div>
            <div>
              <div className="flex items-center gap-x-2">
                <p className="w-20 text-right">网站描述:</p>
                <input
                  className="w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm"
                  placeholder="请输入网站描述"
                  {...register('description', { required: '请输入网站描述' })}
                />
              </div>
              <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.description?.message}</p>
            </div>
            ...

从上面代码我们可以看到,通过以下3步即可管理表单验证:

  1. 定义字段的类型;
  2. useForm中通过defaultValues定义默认值;
  3. form表单的inputregister注册对应的表单值(这里不再需要使用state进行状态管理了,而是由react-hook-form进行管理)
  4. 注册onSumib事件,通过handleSubmit包装,当提交表单事件时,对校验表单。

现在再看页面,当修改不合法的值或者提交表单时,会回显出提示信息,如图:

watch

上面我们从useForm获取的watch方法还未用到,这里举个例子:当我们需要先填了网站标题才能填写网站描述时(未填标题时,禁用处理),怎么实现呢?就需要通过watch来实现,修改代码如下:

tsx 复制代码
  ...
  const subscribeTitle = watch('title');

  const onSubmit: SubmitHandler<FormType> = (data) => {
    console.log('form data:', data);
  }

  return (
    ...
             <div className="flex items-center gap-x-2">
                <p className="w-20 text-right">网站描述:</p>
                <input
                  className={clsx(
                    'w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm',
                    subscribeTitle.trim().length === 0 ? 'bg-gray-300' : ''
                  )}
                  placeholder={subscribeTitle.trim().length === 0 ? '请先填写网站标题' : '请输入网站描述'}
                  disabled={subscribeTitle.trim().length === 0}
                  {...register('description', { required: '请输入网站描述' })}
                />
              </div>
              <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.description?.message}</p>
            </div>
  ...

通过watch监听title的变化,从而对description网站描述进行动态显示,效果图如下:

zod

目前,通过ract-hook-form已经可以进行简单的表单验证和一些动态处理了,但是类型判断和提示信息目前是通过register里面进行的不是很方便。

所以react-hook-form提供了与其他验证库进行搭配使用的方法,这里使用zod验证库进行搭配使用。

打开终端,安装zod依赖:

shell 复制代码
npm i @hookform/resolvers zod

修改之前的代码,如下所示:

tsx 复制代码
'use client';

import { zodResolver } from "@hookform/resolvers/zod";
import clsx from "clsx";
import { SubmitHandler, useForm } from "react-hook-form";
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1, { message: '请输入网站标题' }),
  description: z.string().min(1, { message: '请输入网站描述' })
});

type FormType = z.infer<typeof schema>;

export default function Home() {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors }
  } = useForm({
    resolver: zodResolver(schema),
    defaultValues: {
      title: '',
      description: ''
    }
  });

  const subscribeTitle = watch('title');

  const onSubmit: SubmitHandler<FormType> = (data) => {
    console.log('form data:', data);
  }
...

上面的代码中,重点在于这两段代码:

tsx 复制代码
const schema = z.object({
  title: z.string().min(1, { message: '请输入网站标题' }),
  description: z.string().min(1, { message: '请输入网站描述' })
});

type FormType = z.infer<typeof schema>;

通过定义schema来定义表单的验证规则,通过z.infer来获取表单类型。经过上述的代码修改后,页面也按照之前一样正常运行。

现在有了zod协助,就可以写更出更复杂的限制条件了。

Controller

目前我们通过register方法可以很轻松让react-hook-form给我们管理状态并且验证值的合法性,但是一旦值不是string这类简单的情况时,就不行了。例如:

这是当前定义的zod验证规则:

ts 复制代码
enum Theme {
  DARK = 'dark',
  LIGHT = 'light'
}

const schema = z.object({
  title: z.string().min(1, { message: '请输入网站标题' }),
  description: z.string().min(1, { message: '请输入网站描述' }),
  keywords: z.string().min(1, { message: '请输入关键词' }),
  id: z.string().min(1, { message: '请输入网站ID' }),
  email: z.email({ message: '请输入网站邮箱' }),
  isMaintenance: z.boolean({ message: '请选择是否维护' }),
  createdAt: z.date({ message: '请选择创建时间' }),
  theme: z.enum(Theme, { message: '请选择主题' }),
  logo: z.file({ message: '请上传图片LOGO' }),
});

然后给对应的每个表单组件进行register注册,但页面结果如下:

对于常用的表单组件来说,使用register完全可以,因为他们的值都是string类型,并且可以通过onChange这类事件来修改值。

但是对于dateinput[file]这类组件就不行了,因为:

  1. 我们这里需要的date类型为z.date,但是这里的表单组件返回的是2025/09/22字符串,解决办法可以把z.date改为z.string类型;
  2. 对于input[file]表单组件,他的返回值为FileList类型,对于这里的z.file也不匹配;

所以上面日期组件和图片上传组件都验证不通过,因此我们需要使用react-hook-form提供的Controller组件来更灵活的自定义验证规则,代码修改如下:

tsx 复制代码
const {
    control,
    register,
    watch,
    handleSubmit,
    formState: { errors }
} = useForm({
    resolver: zodResolver(schema),
    defaultValues: {
      title: '',
      description: '',
      keywords: '',
      id: '',
      email: '',
      isMaintenance: undefined,
      createdAt: undefined,
      theme: undefined,
    }
});
...
<div>
  <div className="flex items-center gap-x-2">
    <p className="w-20 text-right">创建日期:</p>
    <Controller
      name="createdAt"
      control={control}
      render={({ field }) => (
        <input
          type="date"
          className="w-64 border border-gray-500 rounded-sm px-2 py-1 text-sm"
          placeholder="请输入网站描述"
          value={field.value ? format(field.value, "yyyy-MM-dd") : ""}
          onChange={e => field.onChange(new Date(e.target.value))}
        />
      )}
    />
  </div>
  <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.createdAt?.message}</p>
</div>
<div>
  <div className="flex items-center gap-x-2">
    <p className="w-20 text-right">网站logo:</p>
    <Controller
      name="logo"
      control={control}
      render={({ field }) => (
        <input
          type="file"
          accept="image/*"
          className="border border-gray-500 rounded-sm p-1 text-sm"
          onChange={e => field.onChange(e.target.files?.[0])}
        />
      )}
    />
  </div>
  <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.logo?.message}</p>
</div>
...

再次测试,结果如下图:

可以看到,现在表单验证就可以通过了,这就是Controller组件的作用,通过name,control,render三个属性就可以更加灵活的进行表单验证。

嵌套

在一些特别的情况下,我们的表单组件可能会有其他表单组件,此时就需要通过嵌套Controller的方式来进行表单验证了,例如下图的友情链接:

上面的友情链接可以增加也可以移除项,每项里面也有input输入框需要验证,此时就需要嵌套Controller进行校验,代码修改如下:

tsx 复制代码
import { Controller, SubmitHandler, useFieldArray, useForm } from "react-hook-form";

const { fields: links, append, remove } = useFieldArray({
    control,
    name: 'links'
});
const onSubmit: SubmitHandler<FormType> = (data) => {
    console.log('form data:', data);
}
...
<div>
  <div className="flex items-baseline gap-x-2">
    <p className="w-20 text-right">友情链接:</p>
    <div className="flex flex-col gap-y-2">
      {
        links.map((item, idx) => (
          <div key={item.id} className="flex items-baseline gap-x-2">
            <Controller
              name={`links.${idx}.name`}
              control={control}
              render={({ field, fieldState }) => (
                <div>
                  <input
                    type="input"
                    className="w-24 border border-gray-500 rounded-sm p-1 text-sm"
                    placeholder="请输入链接名"
                    {...field}
                  />
                  <p className="text-red-600 font-bold text-sm mt-1">{fieldState.error?.message}</p>
                </div>
              )}
            />
            <Controller
              name={`links.${idx}.url`}
              control={control}
              render={({ field, fieldState }) => (
                <div>
                  <input
                    type="input"
                    className="w-48 border border-gray-500 rounded-sm p-1 text-sm"
                    placeholder="请输入链接URL"
                    {...field}
                  />
                  <p className="text-red-600 font-bold text-sm mt-1">{fieldState.error?.message}</p>
                </div>
              )}
            />
            <span
              className="border rounded-full flex justify-center items-center text-[12px] w-5 h-5 text-red-600 border-red-600 cursor-pointer"
              onClick={() => remove(idx)}
            >-</span>
          </div>
        ))
      }
      <button
        type="button"
        className="border px-2 py-1 text-sm rounded-sm cursor-pointer hover:bg-gray-200"
        onClick={() => append({ name: '', url: '' })}
      >添加</button>
    </div>
  </div>
  <p className="text-red-600 font-bold text-sm ml-22 mt-1">{errors.links?.message}</p>
</div>
...

完成后的效果图如下:

最终提交表单,可以看到onSubmit方法中打印的值,如下图:

补充说明

可以看到,上面的错误提示是在每一项后面新增的,因为需要fieldState提供错误信息。如果需要在外层统一处理的话,可以使用createPortal进行渲染。

赋值

当操作表单为编辑时,此时需要初始化表单值,在react-hook-form中也很简单,通过引用useFormreset方法,然后进行表单值的重置即可,例如:

tsx 复制代码
...
const {
    control,
    reset,
    register,
    watch,
    handleSubmit,
    formState: { errors }
} = useForm({
    resolver: zodResolver(schema),
    defaultValues: {
      title: '',
      description: '',
      keywords: '',
      id: '',
      email: '',
      isMaintenance: undefined,
      createdAt: undefined,
      theme: undefined,
      links: []
    }
});
useEffect(() => {
    const initData = async () => {
      const imgUrl = 'https://zod.dev/_next/image?url=%2Flogo%2Flogo-glow.png&w=256&q=100';
      const res = await fetch(imgUrl);
      const blob = await res.blob();
      const file = new File([blob], 'file', { type: blob.type });
      // 模拟异步请求
      const data = await new Promise<FormType>(resolve => setTimeout(() => resolve({
        title: '标题',
        description: '描述',
        keywords: '关键词',
        id: '1',
        email: 'test@email.com',
        isMaintenance: true,
        createdAt: new Date(),
        theme: Theme.DARK,
        logo: file,
        links: [
          { name: 'test', url: 'http://test.com' },
          { name: 'test2', url: 'http://test@email2.com' },
        ]
      }), 2e3))
      reset(data);
    }

    initData();
}, []);
...

完成后打开页面,效果如下图:

可以注意到上面文件没有被赋值,这是因为浏览器的原因。而且上面logo上传的设计是有问题的,这里是因为作为例子才这样使用,实际上应该是上传图片后,通过url来给img标签进行预览的。

结语

到这里,对于react-hook-form的基本使用就差不多了,感兴趣的话可以去react-hook-form看更多的特性和相关内容。

相关推荐
卤代烃2 小时前
[性能优化] 如何高效的获取 base64Image 的 meta 信息
前端·性能优化·agent
IT_陈寒2 小时前
Vite 5大性能优化技巧:构建速度提升300%的实战分享!
前端·人工智能·后端
ssshooter2 小时前
WebGL 整个运行流程是怎样的?shader 是怎么从内存取到值?
前端·webgl
默默地离开2 小时前
小白学习react native 第一天
前端·react native
Mintopia2 小时前
在 Next.js 中开垦后端的第一块菜地:/pages/api 的 REST 接口
前端·javascript·next.js
无羡仙2 小时前
为什么await可以暂停函数的执行
前端·javascript
xw52 小时前
不定高元素动画实现方案(下)
前端·javascript·css
Moment2 小时前
历经4个月,基于 Tiptap 和 NestJs 打造一款 AI 驱动的智能文档协作平台 🚀🚀🚀
前端·javascript·github
JarvanMo2 小时前
Flutter — 在升级到 Flutter SDK 3.35.1 后,如何将 Android SDK 从 35 升级到 36
前端