NextJS13中Tanstack React Table编辑模态框显示最后一行数据问题
问题原因与解决方案
核心问题
- 重复渲染模态框:每一行的
ExpenseActions组件都渲染了一个EditExpenseModal,所有模态框共享同一个isOpen状态。当打开模态框时,最后一个渲染的模态框(对应表格最后一行数据)会覆盖前面的实例,导致始终显示最后一行内容。而删除弹窗是每行独立的,所以能正常对应数据。 - 表单默认值未动态更新:
useForm的defaultValues仅在组件初始化时设置一次,切换编辑行时不会自动更新表单数据。
分步修改方案
1. 重构ExpenseActions.tsx,移除内部模态框
将编辑模态框的控制权交给父组件,只保留删除逻辑和编辑触发事件:
'use client' import { Button } from '@/components/ui/button'; import axios from 'axios'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; interface ExpenseActionsProps { expenseId: string; value: number; description: string; dateOfExpense: Date; onEdit: (expense: {id: string; value: number; description: string; dateOfExpense: Date}) => void; } const ExpenseActions = ({ expenseId, value, description, dateOfExpense, onEdit }: ExpenseActionsProps) => { const router = useRouter(); const deleteExpense = async () => { const isLoading = toast.loading('Eliminando...'); await axios.delete(`/api/expense/${expenseId}`) .then(() => { toast.dismiss(isLoading); toast.success('Item eliminado con éxito!'); router.refresh(); }) .catch(() => { toast.error('Error al eliminar item. Por favor intentelo denuevo.'); }) .finally(() => { toast.dismiss(isLoading); }) } return ( <div className='flex justify-center gap-x-2'> <AlertDialog> <AlertDialogTrigger className='bg-red-700 rounded-lg p-2 text-sm'>Eliminar</AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>¿Está seguro que quiere eliminar el siguiente gasto?: {value}.</AlertDialogTitle> <AlertDialogDescription> Está acción no se puede deshacer. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <Button onClick={deleteExpense} variant='destructive'>Eliminar</Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> <Button size='sm' variant='primary' onClick={() => onEdit({ id: expenseId, value, description, dateOfExpense })}> Modificar </Button> </div> ); } export default ExpenseActions;
2. 在表格父组件中统一管理模态框与编辑状态
将编辑模态框移到表格外部,仅渲染一次,通过状态跟踪当前要编辑的费用数据:
'use client' import { useReactTable, flexRender } from '@tanstack/react-table'; import { columnsExpenses, Expense } from './columns'; import ExpenseActions from './ExpenseActions'; import EditExpenseModal from '@/components/modal/EditExpenseModal'; import { useState } from 'react'; import { useModal } from '@/hooks/useModal'; interface ExpenseTableProps { expenses: Expense[]; } export const ExpenseTable = ({ expenses }: ExpenseTableProps) => { const [currentEditExpense, setCurrentEditExpense] = useState<Expense | null>(null); const { onOpen, isOpen, onClose, type } = useModal(); // 重写操作列,传递编辑回调 const columns = columnsExpenses.map(col => { if (col.id === 'actions') { return { ...col, cell: ({ row }) => ( <ExpenseActions expenseId={row.original.id} value={row.original.value} description={row.original.description} dateOfExpense={row.original.date_of_expense} onEdit={(expense) => { setCurrentEditExpense(expense); onOpen('editExpense'); }} /> ) }; } return col; }); const table = useReactTable({ data: expenses, columns, }); return ( <div> {/* 表格渲染逻辑 */} <table className="w-full"> <thead> {table.getHeaderGroups().map(headerGroup => ( <tr key={headerGroup.id}> {headerGroup.headers.map(header => ( <th key={header.id} className="border p-2"> {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map(row => ( <tr key={row.id} className="border-b"> {row.getVisibleCells().map(cell => ( <td key={cell.id} className="p-2"> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ))} </tbody> </table> {/* 仅渲染一次编辑模态框 */} {currentEditExpense && ( <EditExpenseModal expenseId={currentEditExpense.id} value={currentEditExpense.value} description={currentEditExpense.description} dateOfExpense={currentEditExpense.date_of_expense} isOpen={isOpen && type === 'editExpense'} onClose={() => { onClose(); setCurrentEditExpense(null); }} /> )} </div> ); }
3. 修改EditExpenseModal.tsx,动态更新表单数据
移除内部模态状态依赖,接收外部传递的isOpen和onClose,并在模态框打开时重置表单值:
import * as z from 'zod'; import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { cn } from '@/lib/utils'; import axios from 'axios'; import { useForm } from 'react-hook-form'; import { format } from 'date-fns'; import { CircleDashed, DollarSign, CalendarIcon } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; import { Calendar } from '../ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Input } from '../ui/input'; import { Button } from '../ui/button'; interface EditExpenseModalProps { expenseId:string; value: number; description: string; dateOfExpense: Date; isOpen: boolean; onClose: () => void; } const formSchema = z.object({ value: z.coerce .number({ required_error: 'Valor Requerido.', }) .int({ message: 'El valor debe ser un numero.' }), description: z.string().min(5, { message: 'La descripcion debe tener al menos 5 carácteres' }), dateOfExpense: z.date({ required_error: 'Se requiere una fecha del gasto que realizó' }) }) const EditExpenseModal = ({ expenseId, value, description, dateOfExpense, isOpen, onClose }: EditExpenseModalProps) => { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { value: value, description: description, dateOfExpense:dateOfExpense }, }) const isSubmitting = form.formState.isSubmitting; // 模态框打开时,根据当前选中的费用数据重置表单 useEffect(() => { if (isOpen) { form.reset({ value, description, dateOfExpense }); } }, [isOpen, value, description, dateOfExpense, form]); const onSubmit = async (values: z.infer<typeof formSchema>) => { await axios.patch(`/api/expense/${expenseId}`, values); form.reset(); onClose(); window.location.reload(); } return ( <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent className='bg-white text-black p-0 overflow-hidden dark:bg-[#313338]'> <DialogHeader className='pt-8 px-6'> <DialogTitle className='text-2xl text-center font-bold text-black dark:text-white'> Modificar Gasto. </DialogTitle> </DialogHeader> <div className='p-6'> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className='text-black dark:text-white'> <FormField control={form.control} name='value' render={({ field }) => ( <FormItem> <FormLabel className='text-lg'>Valor Gastado.</FormLabel> <div className='flex flex-row items-center'> <DollarSign size={20} className='absolute' /> <FormControl> <Input type='number' placeholder='Ingrese la cantidad que gastó' className='pl-6 pb-2' disabled={isSubmitting} {...field} /> </FormControl> </div> <FormMessage className='text-red-600' /> </FormItem> )} /> <FormField control={form.control} name='description' render={({ field }) => ( <FormItem className='pt-5'> <FormLabel className='text-lg'>Descripcion del Gasto.</FormLabel> <FormControl> <Input placeholder='Indique pequeña descripcion de lo que gastó' disabled={isSubmitting} {...field} /> </FormControl> <FormDescription> Ingrese el nombre del producto o una pequeña descripcion de lo que gastó. </FormDescription> <FormMessage className='text-red-600' /> </FormItem> )} /> <FormField control={form.control} name="dateOfExpense" render={({ field }) => ( <FormItem className="flex flex-col mt-5"> <FormLabel className='text-lg'>Fecha del Gasto.</FormLabel> <Popover> <PopoverTrigger asChild> <FormControl> <Button variant="outline" className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground" )} > {field.value ? ( format(field.value, "PPP") ) : ( <span>Seleccione una fecha.</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </FormControl> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar mode="single" selected={field.value} onSelect={field.onChange} disabled={(date) => date > new Date() || date < new Date("1900-01-01") } initialFocus /> </PopoverContent> </Popover> <FormDescription> Seleccione la fecha en la que realizó el gasto. </FormDescription> <FormMessage className='text-red-600' /> </FormItem> )} /> <DialogFooter className='px-6 py-4'> { isSubmitting ? ( <Button variant='primary' disabled> <CircleDashed size={20} className='mx-2 animate-spin' /> Modificando... </Button> ) : ( <Button variant='primary' type='submit'> Modificar </Button> ) } </DialogFooter> </form> </Form> </div> </DialogContent> </Dialog> ); } export default EditExpenseModal;
内容的提问来源于stack exchange,提问作者Jose Pablo Arancibia Linker
相关产品推荐
相关产品推荐

