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

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;

问题原因

  1. 手动触发提交的验证流程缺失:直接调用handleSubmit(onSubmit)()不会触发React Hook Form的完整验证逻辑,也无法确保表单字段的最新值同步到表单状态中,导致验证时无法获取到密码的输入值。
  2. 全局键盘事件干扰默认行为: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 22:12:33