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没执行:
- Zod schema中
destination字段设为必填,但表单里没有对应的输入组件,默认值是空字符串,直接触发验证失败,阻止onSubmit执行。 dateRange.to的refine条件错误:val > new Date()是用表单初始化时的时间做对比,而且默认from和to是同一时间,必然验证失败。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 = (
相关产品推荐
相关产品推荐

