skip to Main Content

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"
  }
}

2

Answers


  1. 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.

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

    Login or Signup to reply.
  2. in server action when you are using useFormState, type of first argument of the action and the type of return value have to be same:

     action: (prevState: State, formData: FormData) => void;
    

    prevState has type of State so the return value also should be the same

    action: (prevState: State, formData: FormData) => Promise<State>;
    

    that is because how type of useFormState type is defined:

    function useFormState<State>(action: (state: Awaited<State>) => State | Promise<State>, initialState: Awaited<State>, permalink?: string): [state: Awaited<State>, dispatch: () => void] 
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search