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

React Hook Form+shadcn/ui表单提交无响应问题求助

React Hook Form + shadcn/ui 表单提交无响应,验证无提示

我在学校项目里用React Hook Form结合shadcn/ui开发表单,点击提交按钮时onSubmit完全没反应,连console.log都不输出。怀疑是验证问题,但添加错误提示后也没显示。相关代码和Schema如下:

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useFetch } from "@/hooks/fetcher";
import { AttendanceSchema } from "@/schemas/attendance-schemas";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import React from "react";
import { useForm } from "react-hook-form";
import { useLocation } from "react-router-dom";
import { z } from "zod";

type StudentAttendanceType = {
  nisn: string;
  student_name: string;
};

const statuses = [ "Hadir", "Izin", "Sakit", "Alfa" ];

const Presensi = () => {
  const [generatedId, setGeneratedId] = React.useState("");
  const location = useLocation();
  const scheduleId = location.pathname.split("/")[location.pathname.split("/").length - 1];
  const { data } = useFetch<StudentAttendanceType[]>(
    "http://localhost:8800/backend/attendances/getToAttend/" + scheduleId
  );

  const form = useForm<z.infer<typeof AttendanceSchema>>({
    resolver: zodResolver(AttendanceSchema),
    defaultValues: {
      attendance_list_id: "123",
      attendance: [
        {
          attendance_id: "123",
          status: "Hadir",
          student_id: "123",
          description: "",
        },
      ],
    },
  });

  React.useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await axios.get(
          "http://localhost:8800/backend/attendances/lastId/" + scheduleId
        );
        setGeneratedId(
          parseInt(res.data[0].next_id).toString().padStart(3, "0")
        );
      } catch (error) {
        console.log(error);
      }
    };
    fetchData();
  }, [generatedId, scheduleId]);

  React.useEffect(() => {
    form.reset({
      attendance: data.map((student) => ({
        status: "Hadir",
        student_id: student.student_name,
        description: "",
      })),
    });
  }, [form, data]);

  const onSubmit = async (values: z.infer<typeof AttendanceSchema>) => {
    try {
      await axios.post("http://localhost:8800/backend/attendances/", {
        attendance_list_id: scheduleId + "-" + generatedId,
        schedule_id: scheduleId,
        attendance: data.map((student) => ({
          attendance_id: student.nisn + generatedId,
          student_id: student.nisn,
          status: "Hadir",
          description: ""
        })),
      });
      console.log(values);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className="flex flex-col h-full gap-6 overflow-y-hidden flex-nowrap whitespace-nowrap">
      <h1 className="sm:text-3xl text-2xl font-bold leading-none text-neutral-900">
        Presensi
      </h1>
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)}>
          <div className="flex flex-col justify-between gap-8 py-4 h-full overflow-y-auto">
            {data
              .map((student, index) => (
                <div key={student.nisn}>
                  <FormField
                    control={form.control}
                    name={`attendance.${index}.student_id`}
                    render={({ field }) => (
                      <FormItem className="grid w-full max-w-sm items-center gap-1.5">
                        <FormControl>
                          <Input
                            {...field}
                            className="border-0"
                            disabled
                          />
                        </FormControl>
                        <FormMessage/>
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name={`attendance.${index}.status`}
                    render={({ field }) => (
                      <FormItem className="">
                        <Popover>
                          <PopoverTrigger>
                            <FormControl>
                              <Badge className="w-[100px] flex justify-center">
                                {field.value ? field.value : "Select Status"}
                              </Badge>
                            </FormControl>
                            <FormMessage/>
                          </PopoverTrigger>
                          <PopoverContent className="overflow-hidden p-0">
                            <ul className="text-center">
                              {statuses.map((status) => (
                                <li
                                  key={status}
                                  className="py-0.5 px-2 border-b"
                                  onClick={() => {
                                    form.setValue(
                                      `attendance.${index}.status`,
                                      status
                                    );
                                  }}
                                >
                                  {status}
                                </li>
                              ))}
                            </ul>
                          </PopoverContent>
                        </Popover>
                      </FormItem>
                    )}
                  />
                </div>
              ))}
            <div className="flex justify-end gap-4">
              <Button type="submit">Done</Button>
            </div>
          </div>
        </form>
      </Form>
    </div>
  );
};

export default Presensi;

Schema:

import { z } from "zod";

export const AttendanceSchema = z.object({
  attendance_list_id: z.string().min(1, { message: "List ID is required!" }),
  attendance: z.array(
    z.object({
      attendance_id: z.string().min(1, { message: "ID is required!" }),
      student_id: z.string().min(1, { message: "Student is required!" }),
      status: z.string().min(1, { message: "status is required!" }),
      description: z.optional(z.string().max(225, { message: "Maximum 255 characters!" })),
    })
  )
});

问题分析与修复方案

核心问题

  1. 验证失败拦截提交:Schema要求attendance数组每个对象必须包含attendance_id,但你在form.reset时完全没赋值,默认值也被覆盖,导致验证不通过,onSubmit无法触发。同时attendance_list_id在重置时被忽略,存在验证风险。
  2. 错误提示未绑定:FormMessage没有关联对应字段的错误信息,导致验证失败后无提示。
  3. useEffect依赖错误:第一个useEffect依赖generatedId会引发无限请求循环。
  4. 提交未使用表单真实值:当前提交逻辑直接用data生成数据,忽略用户修改的状态。

修复步骤

1. 完善表单重置逻辑

在重置时补上所有必填字段:

React.useEffect(() => {
  if (!data) return; // 避免data为空时报错
  form.reset({
    attendance_list_id: scheduleId + "-" + generatedId,
    attendance: data.map((student) => ({
      attendance_id: student.nisn + generatedId,
      status: "Hadir",
      student_id: student.student_name,
      description: "",
    })),
  });
}, [form, data, generatedId, scheduleId]);

2. 绑定错误提示到FormMessage

修改FormField的render函数,传递字段错误信息:

// student_id字段示例
<FormField
  control={form.control}
  name={`attendance.${index}.student_id`}
  render={({ field, fieldState }) => (
    <FormItem className="grid w-full max-w-sm items-center gap-1.5">
      <FormControl>
        <Input {...field} className="border-0" disabled />
      </FormControl>
      <FormMessage>{fieldState.error?.message}</FormMessage>
    </FormItem>
  )}
/>

// status字段同理
<FormField
  control={form.control}
  name={`attendance.${index}.status`}
  render={({ field, fieldState }) => (
    <FormItem className="">
      <Popover>
        <PopoverTrigger>
          <FormControl>
            <Badge className="w-[100px] flex justify-center">
              {field.value ? field.value : "Select Status"}
            </Badge>
          </FormControl>
          <FormMessage>{fieldState.error?.message}</FormMessage>
        </PopoverTrigger>
        {/* 其余代码不变 */}
      </Popover>
    </FormItem>
  )}
/>

3. 修正useEffect依赖

移除generatedId依赖,避免无限循环:

React.useEffect(() => {
  const fetchData = async () => {
    try {
      const res = await axios.get(
        "http://localhost:8800/backend/attendances/lastId/" + scheduleId
      );
      setGeneratedId(
        parseInt(res.data[0].next_id).toString().padStart(3, "0")
      );
    } catch (error) {
      console.log(error);
    }
  };
  fetchData();
}, [scheduleId]);

4. 提交时使用表单真实值

改用values获取用户修改后的状态:

const onSubmit = async (values: z.infer<typeof AttendanceSchema>) => {
  try {
    await axios.post("http://localhost:8800/backend/attendances/", {
      attendance_list_id: values.attendance_list_id,
      schedule_id: scheduleId,
      attendance: values.attendance.map((item, index) => ({
        ...item,
        attendance_id: data[index].nisn + generatedId,
        student_id: data[index].nisn // 替换为真实学生ID
      })),
    });
    console.log(values);
  } catch (error) {
    console.log(error);
  }
};

额外调试建议

  • 添加空值判断避免报错:{data?.map((student, index) => (...))}
  • 打印全局错误信息调试:
const { formState: { errors } } = form;
console.log(errors);

内容的提问来源于stack exchange,提问作者Maliki Azis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 14:24:55