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