React Hook Form+Zod登录表单回车提交失效,密码验证异常
按下Enter键提交登录表单时密码验证失败的问题
开发的登录表单支持点击“提交”按钮或按下Enter键完成登录,但按下Enter键提交时,明明已输入密码,却收到“enter your password”的错误提示,表单验证莫名失败。
相关代码
自定义钩子 useKeyDown
import { useEffect } from "react"; export const useKeyDown = (callback, keys) => { const onKeyDown = (e) => { const wasSubmitKeyPressed = keys.some((key) => e.key === key); if (wasSubmitKeyPressed) { e.preventDefault(); callback(); } }; useEffect(() => { document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("keydown", onKeyDown); }; }, [onKeyDown]); };
登录组件 login.tsx
"use client"; import { useState } from "react"; import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; import { useRouter } from "next/navigation"; import { useForm, SubmitHandler } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import Link from "next/link"; import * as z from "zod"; import type { Database } from "lib/database.types"; import { BeatLoader } from "react-spinners"; import Image from "next/image"; import logo from "./../../public/logo.png"; import { useEffect } from "react"; import { useKeyDown } from "hooks/useKeyDown"; type Schema = z.infer<typeof schema>; const schema = z.object({ email: z .string() .nonempty({ message: "Enter your email" }) .email({ message: "Invalid format!" }), password: z.string().nonempty({ message: "Enter your password" }), // .min(6, { message: "Password must be at least six characters!" }), }); const Login = () => { const router = useRouter(); const supabase = createClientComponentClient<Database>(); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(""); const [isButtonFocused, setIsButtonFocused] = useState(false); const [redirectRoute, setRedirectRoute] = useState<string>(""); const [isSubmitted, setIsSubmitted] = useState(false); let storedPathname; console.log("redirectRoute", redirectRoute); const { register, handleSubmit, formState: { errors }, clearErrors, getValues, formState, trigger, } = useForm({ defaultValues: { email: "", password: "" }, resolver: zodResolver(schema), }); console.log("formState", formState); const onSubmit: SubmitHandler<Schema> = async (data) => { console.log("errors", errors); setIsButtonFocused(true); setIsSubmitted(true); try { const { error } = await supabase.auth.signInWithPassword({ email: data.email, password: data.password, }); if (error) { setIsButtonFocused(false); setMessage("An error occurred. " + error.message); return; } router.push(redirectRoute); } catch (error) { setMessage("An error occurred. " + error); return; } finally { setLoading(false); router.refresh(); } }; const keys = ["Enter"]; useKeyDown(() => handleSubmit(onSubmit)(), keys); useEffect(() => { storedPathname = localStorage.getItem("redirectRoute"); console.log("stored pathname: ", storedPathname); if (storedPathname !== null) { setRedirectRoute(storedPathname); console.log("PATHNAME SUCCESS!"); } else { console.log("PATHNAME ERROR!"); setRedirectRoute("/redirect-error-page"); } }, []); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log(e); }; return ( <div className="h-screen pt-10 mt-16 lg:mt-0"> <div className="flex justify-center"> <Image src={logo} alt="logo" height={80} width={120} /> </div> <div className="max-w-[350px] mx-auto border border-gray-300 p-4 px-5"> <div className="mt-4 mb-8 flex flex-col text-3xl font-semibold text-gray-800"> Sign in <div className="mb-4 border border-gray-100 mt-4"></div> </div> <form onSubmit={handleSubmit(onSubmit)} className="mt-6"> <div className="mb-3"> <input type="email" className="border rounded-md w-full py-2 px-3 focus:outline-none focus:border-emerald-500" placeholder="email address" id="email" {...register("email")} onChange={handleInputChange} /> <div className="my-3 text-sm text-red-500"> {errors.email?.message} </div> </div> <div className="mb-5"> <input type="password" className="border rounded-md w-full py-2 px-3 focus:outline-none focus:border-emerald-500" placeholder="password" id="password" {...register("password")} onChange={handleInputChange} /> <div className="my-3 text-sm text-red-500"> {errors.password?.message} </div> </div> <div className="mb-5"> {loading ? ( <div className="w-full flex justify-center"> <BeatLoader speedMultiplier={0.6} /> </div> ) : ( <button type="submit" className={`font-bold bg-amber-300 hover:brightness-95 focus:border focus:border-2 focus:border-emerald-500 focus:opacity-50 ${isButtonFocused ? "border border-2 border-emerald-500 opacity-50" : ""} w-full rounded-md p-2 text-gray-600 text-sm`} > Sign in </button> )} </div> </form> {message && ( <div className="my-5 text-center text-sm text-red-500">{message}</div> )} <div className="text-center text-sm mb-5"> <Link href="/auth/reset-password" className="hover:underline text-blue-500" > Forgot your password? </Link> </div> <div className="text-center text-sm border border-gray-700 rounded-md bg-stone-200 py-1"> <Link href="/auth/signup" className="text-gray-700"> Create an account </Link> </div> </div> </div> ); }; export default Login;
问题原因
- 手动触发提交的验证流程缺失:直接调用
handleSubmit(onSubmit)()不会触发React Hook Form的完整验证逻辑,也无法确保表单字段的最新值同步到表单状态中,导致验证时无法获取到密码的输入值。 - 全局键盘事件干扰默认行为:
useKeyDown监听document层面的Enter键并调用e.preventDefault(),阻止了浏览器默认的表单提交行为,而手动触发的提交没有正确完成字段值的同步与验证。
修复方案
方案1:移除自定义钩子,利用浏览器默认行为
HTML表单本身支持按下Enter键提交,无需额外监听键盘事件。直接删除useKeyDown的相关代码,浏览器会自动触发表单的onSubmit事件,handleSubmit(onSubmit)会正确处理验证和数据获取。
方案2:修复自定义钩子的调用逻辑
如果需要保留自定义键盘监听,需确保触发提交前完成完整验证:
useKeyDown(async () => { const isValid = await trigger(); // 触发所有字段的验证 if (isValid) { onSubmit(getValues()); // 验证通过后获取最新表单值并执行提交 } }, keys);
另外,可删除无实际作用的handleInputChange函数,避免干扰React Hook Form的默认字段更新逻辑。
内容的提问来源于stack exchange,提问作者Dave Odipo
相关产品推荐
相关产品推荐

