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

Shadcn Form的onSubmit未触发问题求助(黑客松紧急支持)

问题:Shadcn Form + React Hook Form 提交时onSubmit函数无法执行

我在黑客松项目里用Shadcn Form和React Hook Form开发了CreateTripForm表单,现在碰到个问题:点提交按钮控制台没报错,按钮点击和表单提交事件都能触发,但React Hook Form的onSubmit函数就是拿不到表单值,内部逻辑完全没执行。

相关代码:

"use client";

import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { api } from "@/trpc/react";
import { DatePickerWithRange } from "@/components/DatePickerWithRange";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormDescription,
  FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Country, State, City } from "country-state-city";
import type { ICity, ICountry, IState } from "country-state-city";

// Zod Schema for Form Validation
const tripFormSchema = z.object({
  name: z.string().min(1, { message: "Name is required." }),
  destination: z.string().min(1, { message: "Destination is required." }),
  country: z.string().min(1, { message: "Country is required." }),
  state: z.string().min(1, { message: "State is required." }),
  city: z.string().min(1, { message: "City is required." }),
  hotelDetails: z.string().optional(),
  dateRange: z.object({
    from: z.date({
      required_error: "Start date is required.",
    }),
    to: z
      .date({
        required_error: "End date is required.",
      })
      .refine((val) => val > new Date(), {
        message: "End date must be after start date.",
      }),
  }),
  flightNumber: z.string().optional(),
});

export function CreateTripForm() {
  const [selectedCountry, setSelectedCountry] = useState<ICountry | null>(null);
  const [selectedState, setSelectedState] = useState<IState | null>(null);
  const [selectedCity, setSelectedCity] = useState<ICity | null>(null);

  const router = useRouter();

  const form = useForm<z.infer<typeof tripFormSchema>>({
    resolver: zodResolver(tripFormSchema),
    defaultValues: {
      name: "",
      destination: "",
      country: "",
      state: "",
      city: "",
      hotelDetails: "",
      dateRange: {
        from: new Date(),
        to: new Date(),
      },
      flightNumber: "",
    },
  });

  const createTrip = api.post.create.useMutation({
    onSuccess: () => {
      router.refresh(); // Refresh the page or navigate if necessary
    },
    onError: (error: unknown) => {
      console.error("Error creating trip:", error);
    },
  });

  function onSubmit(data: z.infer<typeof tripFormSchema>) {
    console.log("Form submitted with data:", data); // Debugging statement

    const { dateRange, ...rest } = data;
    const updatedData = {
      ...rest,
      destination: selectedCity?.name ?? "",
      durationOfStay: Math.ceil(
        (dateRange.to.getTime() - dateRange.from.getTime()) /
          (1000 * 60 * 60 * 24),
      ),
    };
    console.log("Updated data:", updatedData); // Debugging statement
    createTrip.mutate(updatedData);
  }

  const handleCountryChange = (value: string) => {
    const country = Country.getAllCountries().find((c) => c.isoCode === value);
    setSelectedCountry(country ?? null);
    setSelectedState(null); // Reset state and city
    setSelectedCity(null);
    form.setValue("country", value); // Update form state
  };

  const handleStateChange = (value: string) => {
    const state = State.getStatesOfCountry(selectedCountry?.isoCode ?? "").find(
      (s) => s.isoCode === value,
    );
    setSelectedState(state ?? null);
    setSelectedCity(null); // Reset city
    form.setValue("state", value); // Update form state
  };

  const handleCityChange = (value: string) => {
    const city = City.getCitiesOfState(
      selectedState?.countryCode ?? "",
      selectedState?.isoCode ?? "",
    ).find((c) => c.name === value);
    setSelectedCity(city ?? null);
    form.setValue("city", value); // Update form state
  };

  const countries = Country.getAllCountries();
  const states = selectedCountry
    ? State.getStatesOfCountry(selectedCountry.isoCode)
    : [];
  const cities = selectedState
    ? City.getCitiesOfState(selectedState.countryCode, selectedState.isoCode)
    : [];

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="my-10 flex w-full flex-col items-start justify-center gap-y-5 px-10"
      >
        <h1 className="mb-5 text-center text-2xl font-bold">Create a Trip</h1>

        {/* Name Field */}
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input
                  {...field}
                  placeholder="Enter trip name"
                  className="w-full"
                />
              </FormControl>
              <FormMessage>{form.formState.errors.name?.message}</FormMessage>
            </FormItem>
          )}
        />

        {/* Country Selector */}
        <FormField
          control={form.control}
          name="country"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Country</FormLabel>
              <FormControl>
                <Select value={field.value} onValueChange={handleCountryChange}>
                  <SelectTrigger className="w-[280px]">
                    <SelectValue placeholder="Select a country" />
                  </SelectTrigger>
                  <SelectContent>
                    {countries.map((country) => (
                      <SelectItem key={country.isoCode} value={country.isoCode}>
                        {country.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FormControl>
              <FormMessage>
                {form.formState.errors.country?.message}
              </FormMessage>
            </FormItem>
          )}
        />

        {/* State Selector */}
        <FormField
          control={form.control}
          name="state"
          render={({ field }) => (
            <FormItem>
              <FormLabel>State</FormLabel>
              <FormControl>
                <Select
                  value={field.value}
                  onValueChange={handleStateChange}
                  disabled={!selectedCountry}
                >
                  <SelectTrigger className="w-[280px]">
                    <SelectValue placeholder="Select a state" />
                  </SelectTrigger>
                  <SelectContent>
                    {states.map((state) => (
                      <SelectItem key={state.isoCode} value={state.isoCode}>
                        {state.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FormControl>
              <FormMessage>{form.formState.errors.state?.message}</FormMessage>
            </FormItem>
          )}
        />

        {/* City Selector */}
        <FormField
          control={form.control}
          name="city"
          render={({ field }) => (
            <FormItem>
              <FormLabel>City</FormLabel>
              <FormControl>
                <Select
                  value={field.value}
                  onValueChange={handleCityChange}
                  disabled={!selectedState}
                >
                  <SelectTrigger className="w-[280px]">
                    <SelectValue placeholder="Select a city" />
                  </SelectTrigger>
                  <SelectContent>
                    {cities.map((city) => (
                      <SelectItem key={city.name} value={city.name}>
                        {city.name}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FormControl>
              <FormMessage>{form.formState.errors.city?.message}</FormMessage>
            </FormItem>
          )}
        />

        {/* Date Range Field */}
        <FormField
          control={form.control}
          name="dateRange"
          render={({ field }) => (
            <FormItem className="flex flex-col">
              <FormLabel>Date Range</FormLabel>
              <DatePickerWithRange
                className="w-[300px]"
                onChange={(dateRange) => field.onChange(dateRange)}
                value={field.value}
              />
              <FormDescription>
                Choose the date range for your trip.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        {/* Hotel Details Field */}
        <FormField
          control={form.control}
          name="hotelDetails"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Hotel Details</FormLabel>
              <FormControl>
                <Input
                  {...field}
                  placeholder="Enter hotel details"
                  className="w-full"
                />
              </FormControl>
              <FormMessage>
                {form.formState.errors.hotelDetails?.message}
              </FormMessage>
            </FormItem>
          )}
        />

        {/* Flight Number Field */}
        <FormField
          control={form.control}
          name="flightNumber"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Flight Number</FormLabel>
              <FormControl>
                <Input
                  {...field}
                  placeholder="Enter flight number"
                  className="w-full"
                />
              </FormControl>
              <FormMessage>
                {form.formState.errors.flightNumber?.message}
              </FormMessage>
            </FormItem>
          )}
        />

        {/* Submit Button */}
        <Button type="submit" className="mt-4">
          Create Trip
        </Button>
      </form>
    </Form>
  );
}

export default CreateTripForm;

问题分析

根本原因是表单验证未通过,但错误提示没正确显示,导致你误以为onSubmit没执行:

  1. Zod schema中destination字段设为必填,但表单里没有对应的输入组件,默认值是空字符串,直接触发验证失败,阻止onSubmit执行。
  2. dateRange.to的refine条件错误:val > new Date()是用表单初始化时的时间做对比,而且默认from和to是同一时间,必然验证失败。
  3. dateRange的FormMessage没有绑定错误信息,看不到验证失败的提示。

解决方案

1. 调整Zod Schema

移除destination字段(因为你在onSubmit中通过selectedCity.name赋值,不需要表单维护该字段),修正dateRange的验证逻辑:

const tripFormSchema = z.object({
  name: z.string().min(1, { message: "Name is required." }),
  country: z.string().min(1, { message: "Country is required." }),
  state: z.string().min(1, { message: "State is required." }),
  city: z.string().min(1, { message: "City is required." }),
  hotelDetails: z.string().optional(),
  dateRange: z.object({
    from: z.date({
      required_error: "Start date is required.",
    }),
    to: z.date({
      required_error: "End date is required.",
    }),
  }).refine((data) => data.to > data.from, {
    message: "End date must be after start date.",
    path: ["to"], // 指定错误绑定到to字段
  }),
  flightNumber: z.string().optional(),
});

2. 修正表单默认值

去掉defaultValues中的destination,并将默认结束日期设为明天,避免初始验证失败:

defaultValues: {
  name: "",
  country: "",
  state: "",
  city: "",
  hotelDetails: "",
  dateRange: {
    from: new Date(),
    to: new Date(Date.now() + 86400000), // 默认结束日期为明天
  },
  flightNumber: "",
},

3. 修复FormMessage绑定

给dateRange的FormMessage添加错误信息:

<FormMessage>{form.formState.errors.dateRange?.to?.message || form.formState.errors.dateRange?.message}</FormMessage>

4. 可选:添加验证状态提示

在提交按钮上方添加调试信息,方便查看表单验证状态:

{form.formState.isSubmitted && !form.formState.isValid && (
  <p className="text-sm text-red-500">请修正以上错误后提交</p>
)}

修改后的完整代码

"use client";

import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { api } from "@/trpc/react";
import { DatePickerWithRange } from "@/components/DatePickerWithRange";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormDescription,
  FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Country, State, City } from "country-state-city";
import type { ICity, ICountry, IState } from "country-state-city";

// Zod Schema for Form Validation
const tripFormSchema = z.object({
  name: z.string().min(1, { message: "Name is required." }),
  country: z.string().min(1, { message: "Country is required." }),
  state: z.string().min(1, { message: "State is required." }),
  city: z.string().min(1, { message: "City is required." }),
  hotelDetails: z.string().optional(),
  dateRange: z.object({
    from: z.date({
      required_error: "Start date is required.",
    }),
    to: z.date({
      required_error: "End date is required.",
    }),
  }).refine((data) => data.to > data.from, {
    message: "End date must be after start date.",
    path: ["to"],
  }),
  flightNumber: z.string().optional(),
});

export function CreateTripForm() {
  const [selectedCountry, setSelectedCountry] = useState<ICountry | null>(null);
  const [selectedState, setSelectedState] = useState<IState | null>(null);
  const [selectedCity, setSelectedCity] = useState<ICity | null>(null);

  const router = useRouter();

  const form = useForm<z.infer<typeof tripFormSchema>>({
    resolver: zodResolver(tripFormSchema),
    defaultValues: {
      name: "",
      country: "",
      state: "",
      city: "",
      hotelDetails: "",
      dateRange: {
        from: new Date(),
        to: new Date(Date.now() + 86400000), // 默认结束日期为明天
      },
      flightNumber: "",
    },
  });

  const createTrip = api.post.create.useMutation({
    onSuccess: () => {
      router.refresh();
    },
    onError: (error: unknown) => {
      console.error("Error creating trip:", error);
    },
  });

  function onSubmit(data: z.infer<typeof tripFormSchema>) {
    console.log("Form submitted with data:", data);

    const { dateRange, ...rest } = data;
    const updatedData = {
      ...rest,
      destination: selectedCity?.name ?? "",
      durationOfStay: Math.ceil(
        (dateRange.to.getTime() - dateRange.from.getTime()) /
          (1000 * 60 * 60 * 24),
      ),
    };
    console.log("Updated data:", updatedData);
    createTrip.mutate(updatedData);
  }

  const handleCountryChange = (
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 10:09:03