Reputation: 1553
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"
}
}
Upvotes: 0
Views: 958
Reputation: 2956
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
.
Upvotes: 1
Reputation: 49581
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]
Upvotes: 1