reactjsformsnext.jsreact-domserver-action

Using server action useActionState hook in Nextjs


I have a sign in form that is calling a server action and I'm using useActionState to handle the state of the form but I am getting a typescript error

Argument of type '(state: SignInFormInitialState, formData: FormData) => Promise<SignInFormInitialState>' is not assignable to parameter of type '(state: SignInFormInitialState) => SignInFormInitialState | Promise<SignInFormInitialState>'.
  Target signature provides too few arguments. Expected 2 or more, but got 1.ts(2345)
(alias) function signIn(state: SignInFormInitialState, formData: FormData): Promise<SignInFormInitialState>
import signIn

Here is how my code is structured.

My sign in form

"use client";

import signIn from "@/actions/auth/sign-in/sign-in";
import {
  ChangeEvent,
  useActionState,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import CustomInput from "../custom-input";
import CustomButton from "../custom-button";
import { SignInFormInitialState } from "@/actions/auth/sign-in/interface";
import toast from "react-hot-toast";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import {
  Dialog,
  DialogBackdrop,
  DialogPanel,
  DialogTitle,
} from "@headlessui/react";

const initialState: SignInFormInitialState = {
  username: "",
  password: "",
};

interface SignInFormProps {
  login_required: string;
}

export default function SignInForm({ login_required }: SignInFormProps) {
  const [state, formAction, pending] = useActionState<SignInFormInitialState>(
    signIn,
    initialState
  );
  const searchParams = useSearchParams();
  const { replace } = useRouter();
  const pathname = usePathname();
  const toastShown = useRef(false);
  const [passwordLength, setPasswordLength] = useState<number>(0);
  const usernameRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

const handlePasswordLengthChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      setPasswordLength(e.target.value.length);
    },
    []
  );

  useEffect(() => {
    if (login_required && !toastShown.current) {
      toast.error("Kindly Sign In!");
      toastShown.current = true;
      const params = new URLSearchParams(searchParams);
      params.delete("login_required");
      replace(`${pathname}`);
    }
  }, [pathname, replace, searchParams, login_required]);

  //   Focus on input on load
  useEffect(() => {
    if (usernameRef.current?.value === "") {
      usernameRef.current.focus();
    } else if (
      passwordRef.current?.value === "" ||
      (passwordRef.current?.value && passwordRef.current?.value?.length < 8)
    ) {
      passwordRef.current.focus();
    } else {
      // do nothing
    }
  }, [state?.errors?.username, state?.errors?.password]);

  return (
    <form action={formAction} className="flex flex-col gap-4">
      <CustomInput
        label="Username"
        type="text"
        name="username"
        defaultValue={state.username}
        pending={pending}
        ref={usernameRef}
        isLabelError={
          state?.errors?.username && state?.errors?.username.length > 0
        }
        errors={state?.errors}
        errorsName={state?.errors?.username}
      />

      <CustomInput
        label="Password"
        type="password"
        name="password"
        defaultValue={state.password}
        pending={pending}
        ref={passwordRef}
        isLabelError={
          (state?.errors?.password && state?.errors?.password.length > 0) ||
          (state?.errors?.authStatus && state?.errors?.authStatus.length > 0)
        }
        errors={state?.errors}
        errorsName={state?.errors?.authStatus || state?.errors?.password}
        passwordLength={passwordLength}
        onChange={handlePasswordLengthChange}
      />

      <CustomButton
        type="submit"
        pending={pending}
        title="Sign In"
        pendingTitle="Signing In..."
      />
    </form>
  );
}

My server action

"use server";

import { redirect } from "next/navigation";
import { z } from "zod";
import { cookies } from "next/headers";
import { encrypt } from "@/lib/encrypt";
import { PrismaClient } from "@prisma/client";
import { SignInFormInitialState } from "./interface";

const prisma = new PrismaClient();

const signInSchema = z.object({
  username: z.string().min(1, "Username is required"),
  password: z.string().min(8, "Password must be at lease 8 characters long"),
});

export default async function signIn(
  state: SignInFormInitialState,
  formData: FormData
): Promise<SignInFormInitialState> {
  const username = String(formData.get("username"));
  const password = String(formData.get("password"));

  console.log(state);

  const validatedFields = signInSchema.safeParse({
    username,
    password,
  });

  if (!validatedFields.success) {
    return {
      username,
      password,
      errors: validatedFields?.error?.flatten()?.fieldErrors,
    };
  }

  const user = await prisma.user.findUnique({
    where: { username: username },
  });

  if (!user) {
    return {
      username,
      password,
      errors: {
        authStatus: ["User does not exist"],
      },
    };
  }

  const response = await fetch("http://localhost:3000/api/auth/sign-in", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ username, password }),
  });

  if (response.status === 401) {
    return {
      username,
      password,
      errors: {
        authStatus: ["Invalid Username or Password"],
      },
    };
  }

  if (response.status === 500) {
    throw new Error("Something went wrong");
  }

  const result = await response.json();

  const expires = new Date(Date.now() + 3 * 60 * 1000);

  const session = await encrypt({ result, expires });

  const cookieStore = await cookies();

  cookieStore.set("session", session, {
    expires,
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    path: "/",
  });

  redirect("/dashboard/policies");
}

And here is my interface

export interface SignInFormErrors {
  username?: string[];
  password?: string[];
  authStatus?: string[];
}

export interface SignInFormInitialState {
  username: string;
  password: string;
  errors?: SignInFormErrors;
}

And this is my package.json file

"dependencies": {
    "@headlessui/react": "^2.2.0",
    "@prisma/client": "^6.0.1",
    "jose": "^5.9.6",
    "next": "^15.0.3",
    "next-nprogress-bar": "^2.3.15",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-hot-toast": "^2.4.1",
    "react-icons": "^5.4.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@tailwindcss/forms": "^0.5.9",
    "@types/node": "^22.10.1",
    "@types/react": "^19.0.1",
    "@types/react-dom": "^19.0.1",
    "eslint": "^9.16.0",
    "eslint-config-next": "^15.0.4",
    "postcss": "^8.4.49",
    "prisma": "^6.0.1"
  }

When I remove the formData params from the server function, the warning goes away but the formData is required for me to get access to whatever was submitted.

I need assistance on how i can make this error go away.


Solution

  • you should add the second generic parameter in your useActionState declaration to fix typescript error:

    const [state, formAction, pending] = useActionState<SignInFormInitialState, FormData>(
        signIn,
        initialState
      );
    

    enter image description here