You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13中基于React useFieldArray实现多组尺寸(width、height、length)输入字段堆叠的实现方法问询

Next.js 13中基于React useFieldArray实现多组尺寸(width、height、length)输入字段堆叠的实现方法问询

嗨,我正好做过类似的需求,给你详细说说在Next.js 13里用React Hook Form的useFieldArray实现动态多组尺寸输入的具体方案,完全匹配你想要的堆叠效果:

第一步:安装必要依赖

首先得确保项目里装了React Hook Form,毕竟useFieldArray是它的核心功能之一;如果需要做表单校验,还可以搭配Zod这类校验库一起用:

npm install react-hook-form @hookform/resolvers zod
# 用yarn的话就执行下面这条
yarn add react-hook-form @hookform/resolvers zod

第二步:编写客户端组件(重点!)

Next.js 13的App Router默认是服务端组件,而表单交互需要客户端环境,所以一定要给组件加上'use client'指令。下面是完整的可运行示例代码:

'use client';

import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

// 定义单组尺寸的校验规则
const SizeItemSchema = z.object({
  width: z.number().positive('宽度必须是正数'),
  height: z.number().positive('高度必须是正数'),
  length: z.number().positive('长度必须是正数'),
});

// 定义整个表单的校验规则
const FormSchema = z.object({
  sizes: z.array(SizeItemSchema).min(1, '至少需要填写一组尺寸'),
});

// 推导表单值的类型
type FormValues = z.infer<typeof FormSchema>;

export default function MultiSizeForm() {
  // 初始化表单,绑定校验规则和默认值
  const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(FormSchema),
    defaultValues: {
      sizes: [{ width: 0, height: 0, length: 0 }], // 初始化一组默认尺寸,确保页面加载后有输入框
    },
  });

  // 初始化useFieldArray,绑定表单控制器和数组字段名
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'sizes',
  });

  // 表单提交逻辑
  const onSubmit = (data: FormValues) => {
    console.log('提交的所有尺寸数据:', data.sizes);
    // 这里可以写你的业务逻辑,比如调用API提交数据
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-8 max-w-3xl mx-auto">
      <h2 className="text-xl font-bold mb-4">多组尺寸设置</h2>
      
      {/* 循环渲染每一组尺寸输入框 */}
      {fields.map((field, index) => (
        <div key={field.id} className="flex gap-4 items-center p-4 border rounded-lg bg-gray-50">
          {/* 宽度输入框 */}
          <div className="flex-1">
            <label className="block text-sm font-medium mb-1">宽度</label>
            <Controller
              name={`sizes.${index}.width`}
              control={control}
              render={({ field }) => (
                <input
                  type="number"
                  {...field}
                  value={field.value || ''}
                  className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              )}
            />
            {/* 校验错误提示 */}
            {errors.sizes?.[index]?.width && (
              <p className="text-red-500 text-xs mt-1">{errors.sizes[index].width.message}</p>
            )}
          </div>

          {/* 高度输入框 */}
          <div className="flex-1">
            <label className="block text-sm font-medium mb-1">高度</label>
            <Controller
              name={`sizes.${index}.height`}
              control={control}
              render={({ field }) => (
                <input
                  type="number"
                  {...field}
                  value={field.value || ''}
                  className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              )}
            />
            {errors.sizes?.[index]?.height && (
              <p className="text-red-500 text-xs mt-1">{errors.sizes[index].height.message}</p>
            )}
          </div>

          {/* 长度输入框 */}
          <div className="flex-1">
            <label className="block text-sm font-medium mb-1">长度</label>
            <Controller
              name={`sizes.${index}.length`}
              control={control}
              render={({ field }) => (
                <input
                  type="number"
                  {...field}
                  value={field.value || ''}
                  className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              )}
            />
            {errors.sizes?.[index]?.length && (
              <p className="text-red-500 text-xs mt-1">{errors.sizes[index].length.message}</p>
            )}
          </div>

          {/* 删除按钮:只有当组数大于1时才显示 */}
          {fields.length > 1 && (
            <button
              type="button"
              onClick={() => remove(index)}
              className="text-red-500 hover:text-red-700 transition-colors"
            >
              删除
            </button>
          )}
        </div>
      ))}

      {/* 添加新组按钮 */}
      <button
        type="button"
        onClick={() => append({ width: 0, height: 0, length: 0 })}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
      >
        添加一组尺寸
      </button>

      {/* 提交按钮 */}
      <button
        type="submit"
        className="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
      >
        提交表单
      </button>
    </form>
  );
}

第三步:关键细节说明

  • 'use client'指令:在Next.js 13 App Router中必须添加,否则客户端交互相关的API会报错;如果用的是Pages Router,不需要加这行
  • useFieldArray核心方法:
    • fields:存储当前所有的尺寸组数据,用来循环渲染输入框
    • append:添加新的一组尺寸数据
    • remove:删除指定索引的尺寸组
  • Controller组件:搭配Zod校验时,用Controller能更顺畅地实现受控组件和表单状态的同步,同时处理校验逻辑
  • 样式调整:示例用了Tailwind样式,你可以根据自己项目的UI库(比如MUI、Ant Design)修改样式,实现和你GIF里一样的堆叠视觉效果

备注:内容来源于stack exchange,提问作者Amith A G

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 13:53:01