You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS13中Tanstack React Table编辑模态框显示最后一行数据问题

问题原因与解决方案

核心问题

  1. 重复渲染模态框:每一行的ExpenseActions组件都渲染了一个EditExpenseModal,所有模态框共享同一个isOpen状态。当打开模态框时,最后一个渲染的模态框(对应表格最后一行数据)会覆盖前面的实例,导致始终显示最后一行内容。而删除弹窗是每行独立的,所以能正常对应数据。
  2. 表单默认值未动态更新: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 02:19:53