nextjs - 💡(How to fix) Fix Signin form performs native GET submit when clicked during hydration → URL becomes /signin?email=&password= [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#85414Fetched 2026-04-08 02:15:55
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
closed ×1commented ×1issue_type_added ×1labeled ×1

Error Message

useHandleMutationError(signInMutation?.error); Email <span className="text-error-500"></span> Password <span className="text-error-500"></span>

Code Example

Next.js (app router / "use client" signin component)

React + react-hook-form + zod + @hookform/resolvers

PrimeReact UI components

@tanstack/react-query for mutation


 "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-hook-form": "^7.62.0",
"next": "15.4.6",
    "nextjs-toploader": "^3.8.16",

Observed in production build and local production server (npm start) when hydration is delayed

---

"use client";

import { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { InputText } from "primereact/inputtext";
import { Password } from "primereact/password";
import { Checkbox } from "primereact/checkbox";
import { Button } from "primereact/button";
import { useRouter } from "nextjs-toploader/app";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import Image from "next/image";
import { postTologin } from "@/services/auth/auth-service";
import useHandleMutationError from "@/hooks/useHandleMutationError";

// Schema validation
const signInSchema = z.object({
  // email: z.string().email("Invalid email address"),
  email: z.string().refine((value) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    // const phoneRegex = /^\+\d{3}\d{3}\d{3}\d{3}$/; // Matches +256123123123 format
    const phoneRegex = /^\d{12}$/; // e.g., 256700000000

    return emailRegex.test(value) || phoneRegex.test(value);
  }, "Invalid email or phone number. Please use a valid email or phone number format (e.g., [email protected] or 256123123123)."),
  password: z.string().min(6, "Password must be at least 6 characters long"),
  rememberMe: z.boolean().optional(),
});

type SignInFormInputs = z.infer<typeof signInSchema>;

// Mock API function - replace with your actual API call
const signInUser = async (data: SignInFormInputs) => {
  // Simulate API call
  await new Promise((resolve) => setTimeout(resolve, 1500));
  return { success: true, user: data };
};

export default function SignInForm() {
  const queryClient = useQueryClient();
  const router = useRouter();
  const [showPassword, setShowPassword] = useState(false);

  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<SignInFormInputs>({
    resolver: zodResolver(signInSchema),
    defaultValues: {
      email: "",
      password: "",
      rememberMe: false,
    },
  });

  const signInMutation = useMutation({
    mutationFn: (variables: SignInFormInputs) => postTologin(variables),
    onSuccess: (data) => {
      queryClient.invalidateQueries();
      queryClient.invalidateQueries({ queryKey: ["logged-in-user"] });

      router.push("/dashboard");
      // router.push("/dashboard/sellin-charts");

    },
  });

  useHandleMutationError(signInMutation?.error);

  const onSubmit = (data: SignInFormInputs) => {
    signInMutation.mutate(data);
  };

  return (
    <div className="flex flex-col flex-1 lg:w-1/2 w-full">
      <div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
        <div>
          {/* Logo */}
          <div className="flex justify-center mb-4 sm:mb-6">
            <Link href="/" className="block">
              <Image
                width={200}
                height={42}
                src="/nice/logos/nice-logo.png"
                priority={true}
                alt="Nice House of Plastics Logo"
                className="h-8 w-auto sm:h-10 md:h-12 lg:h-10"
              />
            </Link>
          </div>

          <div className="mb-5 sm:mb-8">
            <h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
              Sign In
            </h1>
            <p className="text-sm text-gray-500 dark:text-gray-400">
              Enter your email and password to sign in!
            </p>
          </div>

          <div>
            {/* Sign In Form */}
            <form onSubmit={handleSubmit(onSubmit)}>
              <div className="space-y-6">
                {/* Email Field */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Email <span className="text-error-500">*</span>
                  </label>
                  <Controller
                    name="email"
                    control={control}
                    render={({ field }) => (
                      <InputText
                        id="email"
                        placeholder="[email protected]"
                        className={`w-full ${errors.email ? "p-invalid" : ""}`}
                        {...field}
                      />
                    )}
                  />
                  {errors.email && (
                    <p className="mt-1 text-sm text-red-600">
                      {errors.email.message}
                    </p>
                  )}
                </div>

                {/* Password Field */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Password <span className="text-error-500">*</span>
                  </label>
                  <Controller
                    name="password"
                    control={control}
                    render={({ field }) => (
                      <div className="relative">
                        <Password
                          id="password"
                          placeholder="Enter your password"
                          toggleMask
                          feedback={false}
                          className={`w-full ${errors.password ? "p-invalid" : ""
                            }`}
                          pt={{
                            iconField: {
                              root: {
                                style: { width: "100%" },
                              },
                              style: { width: "100%" },
                            },
                            input: {
                              style: { width: "100%" },
                            },
                            root: {
                              style: { width: "100%" },
                            },
                          }}
                          {...field}
                        />
                      </div>
                    )}
                  />
                  {errors.password && (
                    <p className="mt-1 text-sm text-red-600">
                      {errors.password.message}
                    </p>
                  )}
                </div>

                {/* Remember Me and Forgot Password */}
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-3">
                    <Controller
                      name="rememberMe"
                      control={control}
                      render={({ field }) => (
                        <Checkbox
                          id="rememberMe"
                          checked={!!field.value}
                          onChange={(e) => field.onChange(e.checked)}
                        />
                      )}
                    />
                    <span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
                      Keep me logged in
                    </span>
                  </div>
                  <Link
                    href="/forgot-password"
                    className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
                  >
                    Forgot password?
                  </Link>
                </div>

                {/* Submit Button */}
                <div>
                  <Button
                    type="submit"
                    label="Sign In"
                    icon={
                      signInMutation.isPending
                        ? "pi pi-spin pi-spinner"
                        : undefined
                    }
                    loading={signInMutation.isPending}
                    className="w-full"
                    size="small"
                  />
                </div>
              </div>
            </form>

            {/* Sign Up Link */}
            {/* <div className="mt-5">
              <p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
                Don't have an account?{" "}
                <Link
                  href="/signup"
                  className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
                >
                  Sign Up
                </Link>
              </p>
            </div> */}
          </div>
        </div>
      </div>
    </div>
  );
}
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

i dont have please ive provided all code here

To Reproduce

Reproduction Steps

Build and serve the app (production) or open a slow-network dev build so hydration is delayed:

npm run build npm start

Open the signin page: https://<your-host>/signin (or http://localhost:3000/signin).

While the page is still loading (before React hydrates), click the Sign In button.

Observe the browser navigates to https://<your-host>/signin?email=&password= (native GET form submit).

Note: This is easy to reproduce on a throttled network or a heavy first-load JS chunk.

Current vs. Expected behavior

Expected Behavior

Clicking Sign In should not trigger native form GET submission.

Form submission should be handled by React/react-hook-form and signInMutation.mutate(...).

If JS hasn't hydrated yet, the button should either be disabled or the native submit should be prevented.

Actual Behavior

Browser performs native HTML form submit (GET) and app navigates to ?email=&password= before React submits via the mutation handler.

Provide environment information

Next.js (app router / "use client" signin component)

React + react-hook-form + zod + @hookform/resolvers

PrimeReact UI components

@tanstack/react-query for mutation


 "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-hook-form": "^7.62.0",
"next": "15.4.6",
    "nextjs-toploader": "^3.8.16",

Observed in production build and local production server (npm start) when hydration is delayed

Which area(s) are affected? (Select all that apply)

Not sure

Which stage(s) are affected? (Select all that apply)

Other (Deployed)

Additional context

here is my form

"use client";

import { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { InputText } from "primereact/inputtext";
import { Password } from "primereact/password";
import { Checkbox } from "primereact/checkbox";
import { Button } from "primereact/button";
import { useRouter } from "nextjs-toploader/app";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import Image from "next/image";
import { postTologin } from "@/services/auth/auth-service";
import useHandleMutationError from "@/hooks/useHandleMutationError";

// Schema validation
const signInSchema = z.object({
  // email: z.string().email("Invalid email address"),
  email: z.string().refine((value) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    // const phoneRegex = /^\+\d{3}\d{3}\d{3}\d{3}$/; // Matches +256123123123 format
    const phoneRegex = /^\d{12}$/; // e.g., 256700000000

    return emailRegex.test(value) || phoneRegex.test(value);
  }, "Invalid email or phone number. Please use a valid email or phone number format (e.g., [email protected] or 256123123123)."),
  password: z.string().min(6, "Password must be at least 6 characters long"),
  rememberMe: z.boolean().optional(),
});

type SignInFormInputs = z.infer<typeof signInSchema>;

// Mock API function - replace with your actual API call
const signInUser = async (data: SignInFormInputs) => {
  // Simulate API call
  await new Promise((resolve) => setTimeout(resolve, 1500));
  return { success: true, user: data };
};

export default function SignInForm() {
  const queryClient = useQueryClient();
  const router = useRouter();
  const [showPassword, setShowPassword] = useState(false);

  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<SignInFormInputs>({
    resolver: zodResolver(signInSchema),
    defaultValues: {
      email: "",
      password: "",
      rememberMe: false,
    },
  });

  const signInMutation = useMutation({
    mutationFn: (variables: SignInFormInputs) => postTologin(variables),
    onSuccess: (data) => {
      queryClient.invalidateQueries();
      queryClient.invalidateQueries({ queryKey: ["logged-in-user"] });

      router.push("/dashboard");
      // router.push("/dashboard/sellin-charts");

    },
  });

  useHandleMutationError(signInMutation?.error);

  const onSubmit = (data: SignInFormInputs) => {
    signInMutation.mutate(data);
  };

  return (
    <div className="flex flex-col flex-1 lg:w-1/2 w-full">
      <div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
        <div>
          {/* Logo */}
          <div className="flex justify-center mb-4 sm:mb-6">
            <Link href="/" className="block">
              <Image
                width={200}
                height={42}
                src="/nice/logos/nice-logo.png"
                priority={true}
                alt="Nice House of Plastics Logo"
                className="h-8 w-auto sm:h-10 md:h-12 lg:h-10"
              />
            </Link>
          </div>

          <div className="mb-5 sm:mb-8">
            <h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
              Sign In
            </h1>
            <p className="text-sm text-gray-500 dark:text-gray-400">
              Enter your email and password to sign in!
            </p>
          </div>

          <div>
            {/* Sign In Form */}
            <form onSubmit={handleSubmit(onSubmit)}>
              <div className="space-y-6">
                {/* Email Field */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Email <span className="text-error-500">*</span>
                  </label>
                  <Controller
                    name="email"
                    control={control}
                    render={({ field }) => (
                      <InputText
                        id="email"
                        placeholder="[email protected]"
                        className={`w-full ${errors.email ? "p-invalid" : ""}`}
                        {...field}
                      />
                    )}
                  />
                  {errors.email && (
                    <p className="mt-1 text-sm text-red-600">
                      {errors.email.message}
                    </p>
                  )}
                </div>

                {/* Password Field */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Password <span className="text-error-500">*</span>
                  </label>
                  <Controller
                    name="password"
                    control={control}
                    render={({ field }) => (
                      <div className="relative">
                        <Password
                          id="password"
                          placeholder="Enter your password"
                          toggleMask
                          feedback={false}
                          className={`w-full ${errors.password ? "p-invalid" : ""
                            }`}
                          pt={{
                            iconField: {
                              root: {
                                style: { width: "100%" },
                              },
                              style: { width: "100%" },
                            },
                            input: {
                              style: { width: "100%" },
                            },
                            root: {
                              style: { width: "100%" },
                            },
                          }}
                          {...field}
                        />
                      </div>
                    )}
                  />
                  {errors.password && (
                    <p className="mt-1 text-sm text-red-600">
                      {errors.password.message}
                    </p>
                  )}
                </div>

                {/* Remember Me and Forgot Password */}
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-3">
                    <Controller
                      name="rememberMe"
                      control={control}
                      render={({ field }) => (
                        <Checkbox
                          id="rememberMe"
                          checked={!!field.value}
                          onChange={(e) => field.onChange(e.checked)}
                        />
                      )}
                    />
                    <span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
                      Keep me logged in
                    </span>
                  </div>
                  <Link
                    href="/forgot-password"
                    className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
                  >
                    Forgot password?
                  </Link>
                </div>

                {/* Submit Button */}
                <div>
                  <Button
                    type="submit"
                    label="Sign In"
                    icon={
                      signInMutation.isPending
                        ? "pi pi-spin pi-spinner"
                        : undefined
                    }
                    loading={signInMutation.isPending}
                    className="w-full"
                    size="small"
                  />
                </div>
              </div>
            </form>

            {/* Sign Up Link */}
            {/* <div className="mt-5">
              <p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
                Don't have an account?{" "}
                <Link
                  href="/signup"
                  className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
                >
                  Sign Up
                </Link>
              </p>
            </div> */}
          </div>
        </div>
      </div>
    </div>
  );
}

extent analysis

TL;DR

The issue can be resolved by preventing the default form submission behavior when the React component has not yet hydrated.

Guidance

  • The problem arises because the form is submitting natively before React has a chance to hydrate and handle the submission.
  • To fix this, add the event.preventDefault() call to the onSubmit function to prevent the default form submission behavior.
  • Additionally, consider disabling the submit button until the React component has hydrated to prevent accidental native form submissions.
  • Verify that the fix worked by testing the form submission on a slow network or with a delayed hydration.

Example

const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault(); // Prevent default form submission behavior
  const data: SignInFormInputs = {
    email: event.currentTarget.email.value,
    password: event.currentTarget.password.value,
    rememberMe: event.currentTarget.rememberMe.checked,
  };
  signInMutation.mutate(data);
};

Alternatively, you can use event.preventDefault() in the handleSubmit function provided by react-hook-form:

const { handleSubmit } = useForm({
  // ...
});

<form onSubmit={(event) => {
  event.preventDefault();
  handleSubmit(onSubmit)(event);
}}>
  {/* Form fields */}
</form>

Notes

This solution assumes that the issue is caused by the native form submission behavior. If the issue persists after implementing this fix, further investigation may be necessary.

Recommendation

Apply the workaround by adding event.preventDefault() to the onSubmit function to prevent the default form submission behavior. This will ensure that the form submission is handled by React and prevent accidental native form submissions.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - 💡(How to fix) Fix Signin form performs native GET submit when clicked during hydration → URL becomes /signin?email=&password= [1 comments, 2 participants]