NextJS 14中useActionState类型错误求助
NextJS 14 useActionState类型不匹配问题解决
问题概述
构建NextJS 14应用时,使用useActionState调用createBook函数出现重载不匹配错误:createBook的prevState参数被定义为[State]数组类型,与useActionState期望的单个State对象类型不兼容。
错误信息
Overload 1 of 2, '(action: (state: { errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }) => { ...; } | ... 1 more ... | Promise<...>, initialState: { ...; } | { ...; }, permalink?: string | undefined): [state: ...]', gave the following error. Argument of type '(prevState: [State], formData: FormData) => Promise<{ errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; ... 6 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }>' is not assignable to parameter of type '(state: { errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }) => { ...; } |...'. Target signature provides too few arguments. Expected 2 or more, but got 1. Overload 2 of 2, '(action: (state: { errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }, payload: FormData) => { ...; } | ... 1 more ... | Promise<...>, initialState: { ...; } | { ...; }, permalink?: string | undefined): [state: ...]', gave the following error. Argument of type '(prevState: [State], formData: FormData) => Promise<{ errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; ... 6 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }>' is not assignable to parameter of type '(state: { errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }, payload: Form...'. Types of parameters 'prevState' and 'state' are incompatible. Type '{ errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; } | { ...; }' is not assignable to type '[State]'. Type '{ errors: { image_url?: string[] | undefined; bookid?: string[] | undefined; shelf?: string[] | undefined; title?: string[] | undefined; author?: string[] | undefined; publication_year?: string[] | undefined; ... 4 more ...; total_copies?: string[] | undefined; }; message: string; }' is not assignable to type '[State]'. 10 | export default function Form() { 11 | const initialState: State = { message: null, errors: {} }; > 12 | const [State, formAction] = useActionState(createBook, initialState); | ^ 13 | return ( 14 | <form action={formAction}> 15 | <div className="rounded-md bg-gray-50 p-4 md:p-6"> ELIFECYCLE Command failed with exit code 1.
创建书籍表单代码
// create-form.tsx "use client"; import { createBook, State } from '@/app/lib/book-actions'; import { useActionState } from 'react'; import Link from 'next/link'; import { bookFormFields } from '@/app/lib/book-form-fields'; import { Button } from '@/app/ui/button'; export default function Form() { const initialState: State = { message: null, errors: {} }; const [State, formAction] = useActionState(createBook, initialState); return ( <form action={formAction}> <div className="rounded-md bg-gray-50 p-4 md:p-6"> {bookFormFields.map((field) => ( <div className="mb-4" key={field.id}> <label htmlFor={field.id} className="mb-2 block text-sm font-medium"> {field.label} </label> <div className="relative mt-2 rounded-md"> {field.type === "checkbox" ? ( <input id={field.id} name={field.id} type="checkbox" className="peer block h-5 w-5 rounded border-gray-200 text-blue-600 focus:ring-2 focus:ring-blue-200" aria-describedby={`${field.id}-error`} /> ) : ( <input id={field.id} name={field.id} type={field.type} placeholder={field.placeholder} className="peer block w-full rounded-md border border-gray-200 py-2 px-3 text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-200" aria-describedby={`${field.id}-error`} /> )} </div> <div id={`${field.id}-error`} aria-live="polite" aria-atomic="true"> {State.errors?.[field.id] && State.errors[field.id].map((error: string) => ( <p className="mt-2 text-sm text-red-500" key={error}> {error} </p> ))} </div> </div> ))} </div> <div className="mt-6 flex justify-end gap-4"> <Link href="/dashboard/invoices" className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200" > Cancel </Link> <Button type="submit">Create Book</Button> </div> </form> ); }
创建书籍的Action函数代码
'use server'; import { z } from 'zod'; import { sql } from '@vercel/postgres'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import { signIn } from '@/auth'; import { AuthError } from 'next-auth'; // 表单验证Schema const FormSchema = z.object({ id: z.string(), bookid: z.string({ invalid_type_error: "请输入图书编号", }), shelf: z.string({ invalid_type_error: "请输入书架位置", }), title: z.string({ invalid_type_error: "请输入书名", }), author: z.string({ invalid_type_error: "请输入作者", }), publication_year: z.number({ invalid_type_error: "请输入出版年份", }), genre: z.string({ invalid_type_error: "请输入图书分类", }), price: z.coerce.number() .gt(0, { message: '请输入大于0的价格' }), language: z.string({ invalid_type_error: "请输入语言", }), image_url: z.string({ invalid_type_error: "请输入封面图片URL", }), in_stock: z.boolean(), total_copies: z.number(), date: z.string(), }); export async function authenticate( prevState: { message: string }, formData: FormData, ) { try { await signIn('credentials', formData); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case 'CredentialsSignin': return '登录凭证无效'; default: return '登录出错'; } } throw error; } } const CreateBook = FormSchema.omit({ id: true, date: true }); const UpdateBook = FormSchema.omit({ id: true, date: true }); // 状态类型定义 export type State = { errors?: { bookid?: string[]; shelf?: string[]; title?: string[]; author?: string[]; publication_year?: number[]; genre?: string[]; price?: number[]; language?: string[]; image_url?: string[]; in_stock?: boolean[]; total_copies?: number[]; }; message?: string | null; }; export async function createBook(prevState: [State], formData: FormData) { // 验证表单数据 const validatedFields = CreateBook.safeParse({ bookid: formData.get('bookid'), shelf: formData.get('shelf'), title: formData.get('title'), author: formData.get('author'), publication_year: Number(formData.get('publication_year')), genre: formData.get('genre'), price: Number(formData.get('price')), language: formData.get('language'), image_url: formData.get('image_url'), in_stock: Boolean(formData.get('in_stock')), total_copies: Number(formData.get('total_copies')), }); // 验证失败返回错误信息 if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: '字段缺失,创建图书失败', }; } // 准备数据库插入数据 const { bookid, shelf, title, author, publication_year, genre, price, language, image_url, in_stock, total_copies } = validatedFields.data; const date = new Date().toISOString().split('T')[0]; // 插入数据库 try { await sql` INSERT INTO books ( bookid, shelf, title, author, publication_year, genre, price, language, image_url, in_stock, total_copies) VALUES ( ${bookid}, ${shelf}, ${title}, ${author}, ${publication_year}, ${genre}, ${price}, ${language}, ${image_url}, ${in_stock}, ${total_copies}) `; } catch (error) { // 数据库错误返回提示 return { message: '数据库错误,创建图书失败', }; } // 刷新缓存并跳转 revalidatePath('/dashboard/books'); redirect('/dashboard/books'); }
解决方案
1. 修正createBook函数的prevState参数类型
useActionState传递给action函数的是单个State对象,而非数组。将参数类型从[State]改为State:
// 修改前 export async function createBook(prevState: [State], formData: FormData) { ... } // 修改后 export async function createBook(prevState: State, formData: FormData) { ... }
2. 修正表单组件的变量名冲突
表单中const [State, formAction]的State与导入的类型State重名,导致类型混淆,改为小写变量名:
// 修改前 const [State, formAction] = useActionState(createBook, initialState); // 修改后 const [state, formAction] = useActionState(createBook, initialState);
同时将后续使用State的地方全部替换为state:
{state.errors?.[field.id] && state.errors[field.id].map((error: string) => ( <p className="mt-2 text-sm text-red-500" key={error}> {error} </p> ))}
3. 修正State类型定义的错误字段类型
State类型中,publication_year、price、in_stock、total_copies的错误类型被定义为number[]/boolean[],但Zod验证返回的错误信息都是字符串数组,需统一改为string[]:
// 修改前 export type State = { errors?: { bookid?: string[]; shelf?: string[]; title?: string[]; author?: string[]; publication_year?: number[]; genre?: string[]; price?: number[]; language?: string[]; image_url?: string[]; in_stock?: boolean[]; total_copies?: number[]; }; message?: string | null; }; // 修改后 export type State = { errors?: { bookid?: string[]; shelf?: string[]; title?: string[]; author?: string[]; publication_year?: string[]; genre?: string[]; price?: string[]; language?: string[]; image_url?: string[]; in_stock?: string[]; total_copies?: string[]; }; message?: string | null; };
完成以上修改后,类型不匹配的错误即可解决。
内容的提问来源于stack exchange,提问作者Haseeb Jan Hamraz
相关产品推荐
相关产品推荐

