reactjsnext.jserror-handlingnextjs14server-action

Client component isn't receiving server action response Next.js 14


I'm trying to do error handling in Next.js. I'm using a server action that is passed to a form and invoked when the form is submitted, like this::

Form Component

"use client";
import PasswordInput from "@components/PasswordInput";
import ProvidersLogin from "@components/ProvidersLogin";
import { Button, Divider, Input, Link } from "@nextui-org/react";
import loginCredentials from "@utils/loginCredentials";
import ErrorToast from "@components/ErrorToast";
import { useFormState } from "react-dom";
import { useEffect } from "react";

const initialState = {
  message: "",
};

export default function Login() {
  const [error, formAction, isPending] = useFormState(
    loginCredentials,
    initialState
  );

  useEffect(() => {
    console.log("error", error);
  }, [error]);

  return (
      <div className="flex flex-col gap-4 items-center justify-center w-full h-full min-h-screen">
        <form action={formAction} className="flex flex-col gap-4 min-w-96">
          <h1 className="font-black text-4xl">Login</h1>
          <p className="text-default-500 text-lg">
            Let{"'"}s make it even better
          </p>
          <ProvidersLogin />
          {/* Commented till error handling is fixed, as currently it does nothing */}
          <Divider />
          <Input
            type="email"
            name="email"
            label="Email"
            labelPlacement="outside"
            placeholder="Enter your email"
            size="lg"
            // biome-ignore lint/a11y/noPositiveTabindex: <explanation>
            tabIndex={1}
            required
            isDisabled={isPending}
            // description=""
          />
          <div className="relative flex flex-col w-full">
            <Link
              className="absolute -top-1 tap-highlight-transparent outline-none text-medium text-primary no-underline hover:opacity-80 active:opacity-disabled transition-opacity self-end"
              href="/forgotpassword"
            >
              Forgot Password?
            </Link>
            <PasswordInput
              name="password"
              label="Password"
              placeholder="Enter your password"
              required
              // biome-ignore lint/a11y/noPositiveTabindex: <explanation>
              tabIndex={2}
              isDisabled={isPending}
            />
          </div>
          <div className="flex flex-row gap-1.5 justify-center items-center w-full -mt-2 -mb-1">
            Don{"'"}t have an account?
            <Link href="/signup">Sign Up</Link>
          </div>
          <p>{error?.message}</p>
          <div className="flex flex-row gap-4 items-center justify-between">
            <Button
              as={Link}
              href="/"
              color="danger"
              variant="light"
              isDisabled={isPending}
            >
              Cancel
            </Button>
            <Button type="submit" color="primary" isLoading={isPending}>
              Login
            </Button>
          </div>
        </form>
      </div>
  );
}

Server Action

"use server";
import { signIn } from "@auth";
import { redirect, RedirectType } from "next/navigation";

export default async function loginCredentials(
  prevState: { message: string },
  data: FormData
) {
  try {
    const email = data.get("email")?.toString();
    const password = data.get("password")?.toString();

    if (!email || !password) {
      throw new Error("Please fill in all fields");
    }

    await signIn("credentials", {
      email,
      password,
      redirect: false,
      // callbackUrl: "/dashboard",
    });
  } catch (error) {
    // @ts-ignore
    return { message: error.cause.err.message };
  }
  redirect("/dashboard", RedirectType.replace);
}

Note: I know I'm using error.cause.err.message as in next-auth 5, when an error is thrown by a CredentialsSignIn error in the Credentials provider authorize function (If the signIn function is called on the server-side), the message from the CredentialsSignIn is stored in error.cause.err.message.

the issue is when I tried to implement this the server action didn't return the response object, i.e. the state remains undefined

I even tried an implementation of this without the useFormState: Form Component

"use client";
import PasswordInput from "@components/PasswordInput";
import ProvidersLogin from "@components/ProvidersLogin";
import { Button, Divider, Input, Link } from "@nextui-org/react";
import loginCredentials from "@utils/loginCredentials";
import ErrorToast from "@components/ErrorToast";
import { useFormState } from "react-dom";
import { useEffect } from "react";

const initialState = {
  message: "",
};

export default function Login() {
  const [error, setError] = useState<string>()
  const [isPending, setIsPending] = useState(false)

  useEffect(() => {
    console.log("error", error);
  }, [error]);

  return (
      <div className="flex flex-col gap-4 items-center justify-center w-full h-full min-h-screen">
        <form action={async (data) => {
          setIsPending(true)
          const res = await loginCredentials({message: ""}, data)
          if (res?.message) {
            setError(res.message)
          }
        }} className="flex flex-col gap-4 min-w-96">
          <h1 className="font-black text-4xl">Login</h1>
          <p className="text-default-500 text-lg">
            Let{"'"}s make it even better
          </p>
          <ProvidersLogin />
          {/* Commented till error handling is fixed, as currently it does nothing */}
          <Divider />
          <Input
            type="email"
            name="email"
            label="Email"
            labelPlacement="outside"
            placeholder="Enter your email"
            size="lg"
            // biome-ignore lint/a11y/noPositiveTabindex: <explanation>
            tabIndex={1}
            required
            isDisabled={isPending}
            // description=""
          />
          <div className="relative flex flex-col w-full">
            <Link
              className="absolute -top-1 tap-highlight-transparent outline-none text-medium text-primary no-underline hover:opacity-80 active:opacity-disabled transition-opacity self-end"
              href="/forgotpassword"
            >
              Forgot Password?
            </Link>
            <PasswordInput
              name="password"
              label="Password"
              placeholder="Enter your password"
              required
              // biome-ignore lint/a11y/noPositiveTabindex: <explanation>
              tabIndex={2}
              isDisabled={isPending}
            />
          </div>
          <div className="flex flex-row gap-1.5 justify-center items-center w-full -mt-2 -mb-1">
            Don{"'"}t have an account?
            <Link href="/signup">Sign Up</Link>
          </div>
          <p>{error}</p>
          <div className="flex flex-row gap-4 items-center justify-between">
            <Button
              as={Link}
              href="/"
              color="danger"
              variant="light"
              isDisabled={isPending}
            >
              Cancel
            </Button>
            <Button type="submit" color="primary" isLoading={isPending}>
              Login
            </Button>
          </div>
        </form>
      </div>
  );
}

yet the server action returns undefined, I also checked the the server action is actually invoked and it is invoked.

Please, someone provide a solution to this

Note: I know that useFormState is gonna be replaced by useActionState, I tried it and it's still returning undefined


Solution

  • I finally figured it out.

    the issue was that I was trying to modify the headers in the middleware once I stopped passing the headers to NextResponse.next() the issue was fixed.

    More comprehensive explanation

    I incorrectly added all searchParams to the response headers. Didn't realize they weren't just strings, some were functions. This caused invalid headers, leading to data loss. Console logging shows many headers disappearing after modification. I suspect server actions rely on specific headers to communicate with client components, and their absence prevented data transfer. You may need to investigate further.

    You can still add headers to your response, just make sure they are valid.