reactjstypescriptnext.jsserver-action

Typescript does not accept additional prop from server action function on useFormState


I'm having trouble trying to integrate the Next.js server actions with useFormState (for displaying input errors on client side) and Typescript.

Following their official documentation here, they suggest to add a new prop to the server action function, as for example:

export async function createInvoice(prevState: State, formData: FormData)

In their example, they add the server action function as a first parameter to the useFormState, like this:

const [state, dispatch] = useFormState(createInvoice, initialState);

In my case that is:

const [state, dispatch] = useFormState(action, initialState);

Where action is the server action function received from my form page.

Typescript is complaining about the action type, how to fix it?

No overload matches this call.
  Overload 1 of 2, '(action: (state: { message: null; errors: {}; }) => Promise<{ message: null; errors: {}; }>, initialState: { message: null; errors: {}; }, permalink?: string | undefined): [state: { message: null; errors: {}; }, dispatch: () => void]', gave the following error.
    Argument of type '(prevState: State, formData: FormData) => void' is not assignable to parameter of type '(state: { message: null; errors: {}; }) => Promise<{ message: null; errors: {}; }>'.
      Target signature provides too few arguments. Expected 2 or more, but got 1.
  Overload 2 of 2, '(action: (state: { message: null; errors: {}; }, payload: FormData) => Promise<{ message: null; errors: {}; }>, initialState: { message: null; errors: {}; }, permalink?: string | undefined): [state: ...]', gave the following error.
    Argument of type '(prevState: State, formData: FormData) => void' is not assignable to parameter of type '(state: { message: null; errors: {}; }, payload: FormData) => Promise<{ message: null; errors: {}; }>'.
      Type 'void' is not assignable to type 'Promise<{ message: null; errors: {}; }>'.ts(2769)
(parameter) action: (prevState: State, formData: FormData) => void

Follow my Form component code:

import { useFormState } from "react-dom";
import { State } from "@/types/formState";

type Props = {
  children: React.ReactNode;
  action: string | ((prevState: State, formData: FormData) => void) | undefined;
};

const Form = ({ children, action }: Props) => {
  const initialState = { message: null, errors: {} };

  const [state, dispatch] = useFormState(action, initialState);

  return (
    <form
      action={dispatch}
      className="w-full flex justify-center"
      autoComplete="off"
    >
      <div className={`w-full`}>
        {children}
      </div>
    </form>
  );
};

export default Form;

The page where I call the Form component above:

import { createUserAccount } from "@/actions/createUserAccount";
import Form, { Button, InputText } from "@/components/Form";

type Props = {};

const SignUpPage = (props: Props) => {
  return (
    <Form action={createUserAccount}>
      <div className="items-center mb-4 flex relative">
        <InputText
          name="firstname"
          type="text"
          placeholder="Enter your first name"
          required
        />
      </div>

      <div className="items-center mb-4 flex relative">
        <InputText
          name="lastname"
          type="text"
          placeholder="Enter your last name"
          required
        />
      </div>

      <div className="items-center mb-4 flex relative">
        <Button title="Join Now" type="submit" />
      </div>
    </Form>
  );
};

export default SignUpPage;

And my server action function (createUserAccount):

"use server";

import { State } from "@/types/formState";
import userAccountSchema from "@/validation/schemas/createUserAccount";

export async function createUserAccount(prevState: State, formData: FormData) {
  const parsedData = userAccountSchema.safeParse({
    firstname: formData.get("firstname"),
    lastname: formData.get("lastname"),
  });

  // Check if the parsing was not successful
  if (!parsedData.success) {
    return {
      errors: parsedData.error.flatten().fieldErrors,
    };
  }

  // Process validated data
  // ...
  return { success: true };
}

The code returns the input errors with no problems when tested. The issue apparently is only about the Typescript.

Thank you!

Edit #1

Follow my package.json:

{
  "name": "project_name",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "14.0.4",
    "react": "^18.2.46",
    "react-dom": "^18.2.18",
    "sass": "^1.69.7",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.10.6",
    "@types/react": "^18.2.46",
    "@types/react-dom": "^18.2.18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8.56.0",
    "eslint-config-next": "14.0.4",
    "postcss": "^8.4.33",
    "tailwindcss": "^3.4.0",
    "typescript": "^5.3.3"
  }
}

Solution

  • The correct signature for your action prop would be:

    type Props = {
      action: (prevState: State, formData: FormData) => State | Promise<State>;
      /* ... */
    };
    

    It has to be a function and that function has to return a new State or a promise of a new State.

    It can not be a string.
    You may have thought that it is a string, because you are passing the callback to <form action="...">.
    In vanilla React you would have to use a string for the form action, but in NextJS you can pass functions.

    And the action is a required parameter, so it can not be undefined.