next-learn
next-learn copied to clipboard
createInvoice and updateInvoice should maybe have Promise<State> as return type
I just worked through chapter 14 https://nextjs.org/learn/dashboard-app/improving-accessibility and took note that TypeScript complains when using useFormState
because the action functions (createInvoice
and updateInvoice
) differ in type from the initialState
being passed. I've got my project deployed on Vercel (per the instructions in chapter 6 https://nextjs.org/learn/dashboard-app/setting-up-your-database also see P.S. note on this chapter) and the build log outputs:
[16:39:50.854] Failed to compile.
[16:39:50.855]
[16:39:50.855] ./app/ui/invoices/create-form.tsx:17:29
[16:39:50.855] Type error: No overload matches this call.
[16:39:50.856] Overload 1 of 2, '(action: (state: { errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; } | { message: string; errors?: undefined; }) => { errors: { ...; }; message: string; } | { ...; } | Promise<...>, initialState: { ...; } | { ...; }, permalink?: string | undefined): [state: ...]', gave the following error.
[16:39:50.856] Argument of type '(prevState: State, formData: FormData) => Promise<{ errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; } | { ...; }>' is not assignable to parameter of type '(state: { errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; } | { message: string; errors?: undefined; }) => { errors: { ...; }; message: string; } | { ...; } | Promise<...>'.
[16:39:50.856] Target signature provides too few arguments. Expected 2 or more, but got 1.
[16:39:50.856] Overload 2 of 2, '(action: (state: { errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; } | { message: string; errors?: undefined; }, payload: FormData) => { ...; } | ... 1 more ... | Promise<...>, initialState: { ...; } | { ...; }, permalink?: string | undefined): [state: ...]', gave the following error.
[16:39:50.857] Argument of type '{ message: null; errors: {}; }' is not assignable to parameter of type '{ errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; } | { message: string; errors?: undefined; }'.
[16:39:50.858] Type '{ message: null; errors: {}; }' is not assignable to type '{ errors: { customerId?: string[] | undefined; amount?: string[] | undefined; status?: string[] | undefined; }; message: string; }'.
[16:39:50.858] Types of property 'message' are incompatible.
[16:39:50.858] Type 'null' is not assignable to type 'string'.
[16:39:50.858]
[16:39:50.858] [0m [90m 15 |[39m [36mexport[39m [36mdefault[39m [36mfunction[39m [33mForm[39m({ customers }[33m:[39m { customers[33m:[39m [33mCustomerField[39m[] }) {[0m
[16:39:50.858] [0m [90m 16 |[39m [36mconst[39m initialState [33m=[39m { message[33m:[39m [36mnull[39m[33m,[39m errors[33m:[39m {} }[33m;[39m[0m
[16:39:50.858] [0m[31m[1m>[22m[39m[90m 17 |[39m [36mconst[39m [state[33m,[39m dispatch] [33m=[39m useFormState(createInvoice[33m,[39m initialState)[33m;[39m[0m
[16:39:50.858] [0m [90m |[39m [31m[1m^[22m[39m[0m
[16:39:50.858] [0m [90m 18 |[39m[0m
[16:39:50.858] [0m [90m 19 |[39m [36mreturn[39m ([0m
[16:39:50.858] [0m [90m 20 |[39m [33m<[39m[33mform[39m action[33m=[39m{dispatch}[33m>[39m[0m
[16:39:50.932] Error: Command "npm run build" exited with 1
[16:39:51.223]
(sorry for all the special characters representing colors, it's copied directly from the output)
I found that seemingly the cleanest way to resolve this is to add Promise<State>
as the return type of the functions createInvoice
and updateInvoice
in app/lib/actions.ts
export async function createInvoice(
prevState: State,
formData: FormData
): Promise<State> {
export async function updateInvoice(
id: string,
prevState: State,
formData: FormData
): Promise<State> {
I'm still learning quite a bit, and this might not be the best solution, but it does seem to resolve the errors. My initial instinct as to why this might be wrong is both of those functions end with
revalidatePath("/dashboard/invoices");
redirect("/dashboard/invoices");
Neither of those lines imply a return of State
as far as I can tell, so maybe more information could be provided to clarify what's going on here. I know the Promise
part is because the functions are async
so they can await
the sql
calls.
P.S. per the instructions in chapter 6 when deploying from GitHub to Vercel I receive errors about the missing alt
attributes that are later added in chapter 14. I figured it would be easy to add on my own but I looked ahead in the material to see it was covered in a later chapter and waited patiently, but there was no indication in chapter 6 that this was expected. This topic could warrant a separate issue, and the outcome could either be to cover the alt
attribute requirement sooner so that the Vercel deploy can be successful, or somehow otherwise keep that code commented until it's time to cover accessibility in chapter 14, or at least add some notes in chapter 6 about the deploy failing until you reach chapter 14 to correct the alt
attributes (although that is quite a few chapters to cover between 6 and 14).
My P.S. note may be resolved by https://github.com/vercel/next-learn/pull/468
I had not considered leaving the alt
attributes blank, but that pretty much covers it.
@nemchik, I ran into same issue but the issue actually was that I didn't fully change the createInvoice. In sql try/catch block the earlier version returns string when error but it should return { message: 'Database Error: Failed to Create Invoice.'} instead of just 'Database Error: Failed to Create Invoice.'
Summary, no need to add any return types but fix the try/catch to return what it should return in the new implementation.
Copy paste the code-block from https://nextjs.org/learn/dashboard-app/improving-accessibility without any changes and type errors should be gone. Also same code below:
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status')
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.'
};
}
// Prepare data for insertion into the database
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
// Insert data into the database
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
// If a database error occurs, return a more specific error.
return {
message: 'Database Error: Failed to Create Invoice.'
};
}
// Revalidate the cache for the invoices page and redirect the user.
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
@nemchik, I ran into same issue but the issue actually was that I didn't fully change the createInvoice. In sql try/catch block the earlier version returns string when error but it should return { message: 'Database Error: Failed to Create Invoice.'} instead of just 'Database Error: Failed to Create Invoice.'
Summary, no need to add any return types but fix the try/catch to return what it should return in the new implementation.
Copy paste the code-block from https://nextjs.org/learn/dashboard-app/improving-accessibility without any changes and type errors should be gone. Also same code below:
export async function createInvoice(prevState: State, formData: FormData) { // Validate form using Zod const validatedFields = CreateInvoice.safeParse({ customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status') }); // If form validation fails, return errors early. Otherwise, continue. if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: 'Missing Fields. Failed to Create Invoice.' }; } // Prepare data for insertion into the database const { customerId, amount, status } = validatedFields.data; const amountInCents = amount * 100; const date = new Date().toISOString().split('T')[0]; // Insert data into the database try { await sql` INSERT INTO invoices (customer_id, amount, status, date) VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) `; } catch (error) { // If a database error occurs, return a more specific error. return { message: 'Database Error: Failed to Create Invoice.' }; } // Revalidate the cache for the invoices page and redirect the user. revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices'); }
The only difference between the code you posted there and what i am suggesting is
export async function createInvoice(prevState: State, formData: FormData) {
vs
export async function createInvoice(
prevState: State,
formData: FormData,
): Promise<State> {
When removing the return type on the function I get an error in app/ui/invoices/create-form.tsx
on line 17
https://github.com/vercel/next-learn/blob/e75f71499fd2bc2dbabf60992f423e07d33c15fd/dashboard/final-example/app/ui/invoices/create-form.tsx#L17
The tooltip in the screenshot scrolls, but the contents are essentially what I posted in my original message. this is also the same output I get when running
npm run build
.
Got the same problem when I reached chapter 14. At first I tried your solution but didn't work. When I checked the integrity of my app/ui/invoices/create-form.tsx
file, I found that I had a typo like the following:
create-form.tsx
with typo ❌
// imports ...
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { mesasage: null, error: {} } // <- ** Typo here! Should be "errors", not "error" **
const [state, dispatch] = useFormState(createInvoice, initialState)
return (
<form action={dispatch}>...</form>
)
create-form.tsx
fixed ✅
// imports ...
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { mesasage: null, errors: {} } // <- ** Typo fixed **
// ...
This fixed the type error that you showed, the same I had. No need to add types into either createInvoice
or updateInvoice
from app/lib/actions.ts
.
Got the same problem when I reached chapter 14. At first I tried your solution but didn't work. When I checked the integrity of my
app/ui/invoices/create-form.tsx
file, I found that I had a typo like the following:
create-form.tsx
with typo ❌// imports ... export default function Form({ customers }: { customers: CustomerField[] }) { const initialState = { mesasage: null, error: {} } // <- ** Typo here! Should be "errors", not "error" ** const [state, dispatch] = useFormState(createInvoice, initialState) return ( <form action={dispatch}>...</form> )
create-form.tsx
fixed ✅// imports ... export default function Form({ customers }: { customers: CustomerField[] }) { const initialState = { mesasage: null, errors: {} } // <- ** Typo fixed ** // ...
This fixed the type error that you showed, the same I had. No need to add types into either
createInvoice
orupdateInvoice
fromapp/lib/actions.ts
.
I do not have this spelling error. I tested again today removing the type (: Promise<State>
) and it continues to cause the error I posted above.
What's the fix?
It appears there has not yet been a fix.
Yeah im getting the same issue. Error is solely around the useFormState in the edit-form.tsx. It doesn't throw a runtime error. The Create Invoice button does nothing and there are no Aria warnings for amount or status in the invoice. Although the Customer Name warning works ok.
Actually to fix my problem I just found I was missing an import statement at the top of the code.
import { useFormState } from 'react-dom';
I'm not missing the import, still have the issue.
Update: I am in the same situation. Compile correctly but the error is there.
Hello. I am not following this course but I am having exactly the same issue in a personal project. Any fixes found?
Ok, I found a solution for my case, not sure if it's the same that you guys were seeing.
After revalidatePath
I needed a return setting errors and message.
revalidatePath('/home/sessions/add');
return { errors: {}, messages: 'User created' };
or using a redirect.
revalidatePath('/home/sessions/add');
redirect("/home")
If you have a try catch block for your data mutation, you need the return in both cases, inside the try block and inside the catch block
The TypeScript error in app/ui/invoices/create-form.tsx can be fixed by replacing the null
value for the message property in initialState with an empty string ''
const initialState = { message: '', error: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
The TypeScript error in app/ui/invoices/create-form.tsx can be fixed by replacing the
null
value for the message property in initialState with an empty string''
const initialState = { message: '', error: {} }; const [state, dispatch] = useFormState(createInvoice, initialState);
This is the first solution anyone has posted that actually worked for me. I'm kind of shocked that this is what made it work, and also shocked that this issue has remained open for so long.
can be fixed by replacing the
null
value for the message property in initialState with an empty string''
message values can be a string | null
as we are setting the State type in actions.ts
.
referencing an issue here
Thank you.
useFormState
Thank you for this context! This actually makes it make more sense, but there's still an error thrown when using
const initialState = { message: null, errors: {} };
However this is actually fixed by using
import { type State } from '@/app/lib/actions';
...
const initialState: State = { message: null, errors: {} };
Doing this in both create-form.tsx
and edit-form.tsx
resolves the error being thrown without needing to define the Promise<State>
return type on the createInvoice
or updateInvoice
functions, or having to use an empty string for the message
.
However this is actually fixed by using
Yes, That works too.
I have followed every instruction from Chapter 1 to Chapter 16 in this tutorial and bumped into small errors but completed the tutorial. Here is the Final Live Demo Codes : GaneshSrambikal/nextjs_from_offical
This is my package.json:
{
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .",
"start": "next start",
"seed": "node -r dotenv/config ./scripts/seed.js",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.0.18",
"@tailwindcss/forms": "^0.5.7",
"@types/node": "20.5.7",
"@vercel/postgres": "^0.5.1",
"autoprefixer": "10.4.15",
"babel": "^6.23.0",
"bcrypt": "^5.1.1",
"clsx": "^2.0.0",
"next": "^14.0.2",
"next-auth": "^5.0.0-beta.18",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"use-debounce": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bcrypt": "^5.0.1",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.14",
"@vercel/style-guide": "^5.0.1",
"dotenv": "^16.3.1",
"eslint": "^8.52.0",
"eslint-config-next": "^14.0.0",
"eslint-config-prettier": "9.0.0",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "0.5.4"
},
"engines": {
"node": ">=18.17.0"
}
}
Here's my package.json
{
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .",
"seed": "node -r dotenv/config ./scripts/seed.js",
"start": "next start"
},
"dependencies": {
"@heroicons/react": "^2.1.3",
"@tailwindcss/forms": "^0.5.7",
"@types/node": "^20.12.11",
"@vercel/postgres": "^0.8.0",
"autoprefixer": "^10.4.19",
"bcrypt": "^5.1.1",
"clsx": "^2.1.1",
"next": "^14.2.3",
"next-auth": "^5.0.0-beta.17",
"postcss": "^8.4.38",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"use-debounce": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bcrypt": "5.0.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@vercel/style-guide": "6.0.0",
"dotenv": "16.4.5",
"eslint": "8.57.0",
"eslint-config-next": "14.2.3",
"eslint-config-prettier": "9.1.0",
"prettier": "3.2.5",
"prettier-plugin-tailwindcss": "0.5.14"
},
"engines": {
"node": "20.x",
"npm": "10.x"
}
}
I do have some different package versions. Mostly it's been updated by renovate bot for me. Also I notice you have babel
and I do not, and nor does https://github.com/vercel/next-learn/blob/b7a4366f682163a6643416c9cdf2d9c43d0fc542/dashboard/final-example/package.json
The issue is mostly noticeable in vscode, before even deploying anything to vercel.
Hi I ran into the same issue but my case is a bit different. I don't use injected SQL in the createInvoice function but call an API since I built my own server. I tried everything this thread suggested:
- changed null to '' in const initialState = { message: null, errors: {} };
- added Promise<State>
- added return { message: 'Database Error: Failed to Create Invoice.'}
- checked type in const initialState = { message: null, errors: {} }; and confirmed I wrote errors with s, not error
- checked import and already import useFormState
- tried const initialState: State = { message: null, errors: {} };
- confirmed message is string | null NOTHING worked. Really frustrated...
It's weird that I had no problem following the tutorial. At first I did see this error but when I pasted all the code in the tutorial, the issue went away. But when I applied the code to my own project (which is just like the code in the tutorial), I got stuck in this error and couldn't find any fix or understand why it happened. Here's my code. I really appreciate if anyone can come up with a solution.
create-order.tsx (it is create-form.tsx in the tutorial)
"use client";
import Link from "next/link";
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from "@heroicons/react/24/outline";
import { Button } from "@/app/ui/buttons";
import { createInvoice } from "@/app/lib/actions";
import { useFormState } from "react-dom";
import { type State } from "@/app/lib/actions";
export default function Form() {
const initialState: State = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
....
actions.ts
export type OrderState = {
errors?: {
amount?: string[];
status?: string[];
};
message?: string | null;
};
export async function createInvoice(prevState: OrderState, formData: FormData) {
// Validate form fields using Zod
const validatedFields = CreateOrder.safeParse({
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
validatedFields.data.amount *= 100;
const orderData = validatedFields.data;
try {
const response = await fetch(
"http://localhost:4000/dashboard/order/new",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(orderData),
}
);
console.log(response);
if (!response.ok) {
throw new Error("Faile to create new order");
}
const responseData = await response.json();
console.log(responseData);
if (!responseData || !responseData.product || !responseData.product._id) {
throw new Error(
"No response data or no responseData.product or no responseData.product._id"
);
}
console.log("Order created successfully", responseData);
} catch (error) {
return {
message: 'Database Error: Failed to Create Product.'
};
}
// Revalidate the cache for the invoices page and redirect the user.
// revalidatePath('/dashboard/invoices');
// redirect('/dashboard/invoices');
}
Hi I ran into the same issue but my case is a bit different. I don't use injected SQL in the createInvoice function but call an API since I built my own server. I tried everything this thread suggested:
- changed null to '' in const initialState = { message: null, errors: {} };
- added Promise
- added return { message: 'Database Error: Failed to Create Invoice.'}
- checked type in const initialState = { message: null, errors: {} }; and confirmed I wrote errors with s, not error
- checked import and already import useFormState
- tried const initialState: State = { message: null, errors: {} };
- confirmed message is string | null NOTHING worked. Really frustrated...
It's weird that I had no problem following the tutorial. At first I did see this error but when I pasted all the code in the tutorial, the issue went away. But when I applied the code to my own project (which is just like the code in the tutorial), I got stuck in this error and couldn't find any fix or understand why it happened. Here's my code. I really appreciate if anyone can come up with a solution.
create-order.tsx (it is create-form.tsx in the tutorial)
"use client"; import Link from "next/link"; import { CheckIcon, ClockIcon, CurrencyDollarIcon, UserCircleIcon, } from "@heroicons/react/24/outline"; import { Button } from "@/app/ui/buttons"; import { createInvoice } from "@/app/lib/actions"; import { useFormState } from "react-dom"; import { type State } from "@/app/lib/actions"; export default function Form() { const initialState: State = { message: null, errors: {} }; const [state, dispatch] = useFormState(createInvoice, initialState); return ( <form action={dispatch}> <div className="rounded-md bg-gray-50 p-4 md:p-6"> ....
actions.ts
export type OrderState = { errors?: { amount?: string[]; status?: string[]; }; message?: string | null; }; export async function createInvoice(prevState: OrderState, formData: FormData) { // Validate form fields using Zod const validatedFields = CreateOrder.safeParse({ amount: formData.get('amount'), status: formData.get('status'), }); // If form validation fails, return errors early. Otherwise, continue. if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: 'Missing Fields. Failed to Create Invoice.', }; } validatedFields.data.amount *= 100; const orderData = validatedFields.data; try { const response = await fetch( "http://localhost:4000/dashboard/order/new", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(orderData), } ); console.log(response); if (!response.ok) { throw new Error("Faile to create new order"); } const responseData = await response.json(); console.log(responseData); if (!responseData || !responseData.product || !responseData.product._id) { throw new Error( "No response data or no responseData.product or no responseData.product._id" ); } console.log("Order created successfully", responseData); } catch (error) { return { message: 'Database Error: Failed to Create Product.' }; } // Revalidate the cache for the invoices page and redirect the user. // revalidatePath('/dashboard/invoices'); // redirect('/dashboard/invoices'); }
Hey, As per your code mentioned above, there are two mistakes you made which can be fixed:
-
In Create-form.tsx( in your case create-order.tsx )
const initialState: State = { message: null, errors: {} };
Here you are defining type ofinitialState as State
which is not needed. You can fix this by replacing it with thisconst initialState = { message: null, errors: {} };
-
In Create-form.tsx( in your case create-order.tsx ) there are missing parameters in Form function which is { customers }: { customers: CustomerField[] }
I Hope this fixes your code. Happy coding Thank you.
Hey, As per your code mentioned above, there are two mistakes you made which can be fixed:
- In Create-form.tsx( in your case create-order.tsx )
const initialState: State = { message: null, errors: {} };
Here you are defining type ofinitialState as State
which is not needed. You can fix this by replacing it with thisconst initialState = { message: null, errors: {} };
- In Create-form.tsx( in your case create-order.tsx ) there are missing parameters in Form function which is { customers }: { customers: CustomerField[] }
I Hope this fixes your code. Happy coding Thank you.
Thanks for your comment. Regarding the parameter { customers }: { customers: CustomerField[] }, I intentionally left it out because in my project, I don't need such data. Anyway, I managed to fix the error No overload matches this call. But now, I run into another issue, which is Property 'errors' does not exist on type '{ message: string; }'.ts(2339). Even though I have declared both message and errors in the initialState variable.
const initialState = { message: "", errors: {} };
const [state, dispatch] = useActionState(createProduct, initialState);