I'm working on a project using NextJS 13. I have a Tanstack Data Table and I'm using cell actions to edit or delete a row.
When I click on "Modify" button (Blue), It opens a modal to edit the fields corresponding to the row.
However, the problem is that no matter which row I select to modify, it always displays the data of the last row in its corresponding inputs.
import { ColumnDef } from '@tanstack/react-table'
import ExpenseActions from './ExpenseActions';
export type Expense = {
id: string;
description: string;
value: number;
date_of_expense: Date;
}
export const columnsExpenses: ColumnDef<Expense>[] = [
{
accessorKey: 'description',
header: 'Descripcion',
},
{
accessorKey: 'value',
header: 'Gasto',
cell: ({ row }) => {
const amount = parseFloat(row.getValue('value'))
const formatted = new Intl.NumberFormat('es-CL', {
style: 'currency',
currency: 'CLP'
}).format(amount);
return <div className='text-right font-medium'>{formatted}</div>
}
},
{
accessorKey: 'date_of_expense',
header: 'Fecha Gasto',
cell: ({ row }) => {
const day = row.original.date_of_expense.getDate();
const month = row.original.date_of_expense.getMonth() + 1;
const year = row.original.date_of_expense.getFullYear();
return `${day}/${month}/${year}`;
}
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => (
<ExpenseActions
expenseId={row.original.id}
value={row.original.value}
description={row.original.description}
dateOfExpense={row.original.date_of_expense}
/>
)
}
]
'use client'
import { Button } from '@/components/ui/button';
import axios from 'axios';
import { Expense } from '@prisma/client';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import toast from 'react-hot-toast';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useModal } from '@/hooks/useModal';
import EditExpenseModal from '@/components/modal/EditExpenseModal';
import { Row } from '@tanstack/react-table';
import { Payment } from './columns';
interface ExpenseActionsProps {
expenseId: string;
value: number;
description: string;
dateOfExpense: Date;
}
const ExpenseActions = ({
expenseId,
value,
description,
dateOfExpense
}: ExpenseActionsProps) => {
const router = useRouter();
const { onOpen, isOpen } = useModal();
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 (
<>
<EditExpenseModal
expenseId={expenseId}
value={value}
description={description}
dateOfExpense={dateOfExpense}
/>
<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 id='edit-expense' onClick={() => { onOpen('editExpense') }} size='sm' variant='primary'>
Modificar
</Button>
</div>
</>
);
}
export default ExpenseActions;
import * as z from 'zod';
import { useEffect, useState } 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 { useModal } from '@/hooks/useModal';
import { CircleDashed, DollarSign, CalendarIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } 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 { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
interface EditExpenseModalProps {
expenseId:string;
value: number;
description: string;
dateOfExpense: Date;
}
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
}: EditExpenseModalProps) => {
const [isMounted, setIsMounted] = useState(false);
const { isOpen, onClose, type, onOpen } = useModal();
const isModalOpen = isOpen && type === 'editExpense';
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
value: value,
description: description,
dateOfExpense:dateOfExpense
},
})
const isSubmitting = form.formState.isSubmitting;
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await axios.patch(`/api/expense/${expenseId}`, values);
form.reset();
onClose();
window.location.reload();
}
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return (
<Dialog open={isModalOpen} 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ó'
defaultValue={value}
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ó'
defaultValue={description}
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
id='calendar'
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={dateOfExpense}
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;
The modal component only gets the last values of the Table. However, when I click on the "Delete" button (Red), the printed value gets the correct value of the row.
What I'm missing here?.
Thank you very much in advance.
Problem :
However, the problem is that no matter which row I select to modify, it always displays the data of the last row in its corresponding inputs. The modal component only gets the last values of the Table.
Possible Cause :
Not passing other values,
Solution :
Check this Line In your ExpenseActions.tsx
file :
<AlertDialogTitle>¿Está seguro que quiere eliminar el siguiente gasto?: {value}.</AlertDialogTitle>
If you look at the code closely, you haven't used other props {expenseId,description,dateOfExpense}
in <AlertDialog>
just used
{value}
Regarding :
However, when I click on the "Delete" button (Red), the printed value gets the correct value of the row.
Explaination :
Look in DataTable columns definition
you have passed,
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => (
<ExpenseActions
expenseId={row.original.id}
value={row.original.value}
description={row.original.description}
dateOfExpense={row.original.date_of_expense}
/>
)
}
In your ExpenseActions.tsx
has props
{
expenseId,
value,
description,
dateOfExpense
}
deleteExpense
function is using that prop
await axios.delete(`/api/expense/${expenseId}`)
If you still have doubts or if I didn't solved your problem then Please leave a comment (i will update the answer).