1

What is the best practice to fetch data for Tanstack table? I have a function in the backend (NodeJS) which receives page number and page size and returns an array of data.

I want to fetch the data again when changing the page number or page size, but to not use states in the parent component. Or is it better to do that using the front end?

The page.tsx looks like this:

import { Trainee, columns } from "./columns"
import DataTable from "./data-table"

async function getTraineeData(page: number, pageSize: number): Promise<Trainee[]> {
    try {
        const response = await fetch(`....(fetch url)/${page}&limit=${pageSize}`, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        });
        if (!response.ok) {
            throw new Error('An error occurred while trying to fetch table data');
        }
        const data = await response.json();
        console.log('data', data)
        return data;
    }
    catch (err) {
        console.log(err);
        return [];
    }
}

const TraineeTable = async () => {

    const data = await getTraineeData(0, 10);


    return (
        <div className="container mx-auto py-10">
            <DataTable columns={columns} data={data} />
        </div>
    )
}

export default TraineeTable;

then child component DatTtable looks like this:

'use client'

import { ColumnDef, flexRender, useReactTable, getCoreRowModel, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel, VisibilityState } from '@tanstack/react-table';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import * as React from "react"
import { Trainee, columnTranslations } from './columns';
import { BiChevronDown } from 'react-icons/bi';

interface DataTableProps<TData, TValue> {
    columns: ColumnDef<TData, TValue>[]
    data: TData[]
}

const DataTable = <TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) => {
    const [sorting, setSorting] = React.useState<SortingState>([])
    const [pageSize, setPageSize] = React.useState(10);
    const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
        []
    )
    const [columnVisibility, setColumnVisibility] =
        React.useState<VisibilityState>({})
    const [rowSelection, setRowSelection] = React.useState({})

    const table = useReactTable({
        columns,
        data,
        getCoreRowModel: getCoreRowModel(),
        getPaginationRowModel: getPaginationRowModel(),
        getSortedRowModel: getSortedRowModel(),
        onSortingChange: setSorting,
        onColumnFiltersChange: setColumnFilters,
        getFilteredRowModel: getFilteredRowModel(),
        onColumnVisibilityChange: setColumnVisibility,
        onRowSelectionChange: setRowSelection,
        state: {
            sorting,
            columnFilters,
            columnVisibility,
            rowSelection,
        },
    })

    return (
        <div className='w-full'>
            <div className="flex items-center py-4 gap-5">
                <Input
                    placeholder="פילטור לפי שם"
                    value={(table.getColumn("fullName")?.getFilterValue() as string) ?? ""}
                    onChange={(event) =>
                        table.getColumn("fullName")?.setFilterValue(event.target.value)
                    }
                    className="max-w-sm rounded"
                />
                <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                        <Button variant="outline" className="ml-auto rounded">
                            <BiChevronDown className="ml-2 h-4 w-4" />columns
                        </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align="end">
                        {table
                            .getAllColumns()
                            .filter(
                                (column) => column.getCanHide()
                            )
                            .map((column) => {
                                return (
                                    <DropdownMenuCheckboxItem
                                        key={column.id}
                                        className="capitalize"
                                        checked={column.getIsVisible()}
                                        onCheckedChange={(value) =>
                                            column.toggleVisibility(!!value)
                                        }
                                        dir="rtl"
                                    >
                                        {columnTranslations[column.id] || column.id}
                                    </DropdownMenuCheckboxItem>
                                )
                            })}
                    </DropdownMenuContent>
                </DropdownMenu>
            </div>
            <div className="rounded-md border px-2">
                <Table>
                    <TableHeader>
                        {table.getHeaderGroups().map((headerGroup) => (
                            <TableRow key={headerGroup.id}>
                                {headerGroup.headers.map((header) => {
                                    return (
                                        <TableHead key={header.id}>
                                            {header.isPlaceholder
                                                ? null
                                                : flexRender(
                                                    header.column.columnDef.header,
                                                    header.getContext()
                                                )}
                                        </TableHead>
                                    )
                                })}
                            </TableRow>
                        ))}
                    </TableHeader>
                    <TableBody>
                        {table.getRowModel().rows?.length ? (
                            table.getRowModel().rows.map((row) => (
                                <TableRow
                                    key={row.id}
                                    data-state={row.getIsSelected() && "selected"}
                                >
                                    {row.getVisibleCells().map((cell) => (
                                        <TableCell key={cell.id}>
                                            {flexRender(cell.column.columnDef.cell, cell.getContext())}
                                        </TableCell>
                                    ))}
                                </TableRow>
                            ))
                        ) : (
                            <TableRow>
                                <TableCell colSpan={columns.length} className="h-24 text-center">
                                    Nothing to show here.
                                </TableCell>
                            </TableRow>
                        )}
                    </TableBody>
                </Table>
            </div>
            {table.getFilteredSelectedRowModel().rows.length > 0 && (<div className="flex-1 mt-2 text-sm text-muted-foreground">
                {table.getFilteredSelectedRowModel().rows.length} of{" "}
                {table.getFilteredRowModel().rows.length} lines selected
            </div>)
            }
            <div className="flex items-center justify-center gap-5 space-x-2 py-4">
                <Button
                    variant="outline"
                    size="sm"
                    className="text-green2 cursor-pointer"
                    onClick={() => table.previousPage()}
                    disabled={!table.getCanPreviousPage()}
                >
                    previous
                </Button>
                <Button
                    variant="outline"
                    size="sm"
                    className="text-green2 cursor-pointer"
                    onClick={() => table.nextPage()}
                    disabled={!table.getCanNextPage()}
                >
                    next
                </Button>
                <span className="flex items-center gap-1">
                    <div>page</div>
                    <strong>
                        {table.getState().pagination.pageIndex + 1}</strong> מתוך{" "}
                    <strong>{table.getPageCount()}</strong>
                </span>
                <span className="flex items-center gap-1">
          move to page:
          <input
            type="number"
            defaultValue={table.getState().pagination.pageIndex + 1}
            onChange={(e) => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              table.setPageIndex(page);
            }}
            className="border p-1 rounded w-16 bg-transparent"
          />
        </span>
        <select
          value={table.getState().pagination.pageSize}
          onChange={(e) => {
            setPageSize(Number(e.target.value));
            table.setPageSize(Number(e.target.value));
          }}
          className="p-2 bg-transparent"
        >
          {[10, 20, 30, 50].map((pageSize) => (
            <option key={pageSize} value={pageSize}>
              show {pageSize}
            </option>
          ))}
        </select>
            </div>
        </div>
    )
};

export default DataTable;

I am confused how to do it correctly.

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
steve123
  • 31
  • 3
  • Thank you, so why do I need the data from the parent component if so ? also, how do I add filters for header columns? (different component column.tsx) for example I want to filter by gender (male and female) { header: () =>
    {columnTranslations.gender}
    , accessorKey: "gender", cell: ({ row }) =>
    {row.getValue("gender")}
    , },
    – steve123 Aug 12 '23 at 17:38

1 Answers1

1

To update the data in DataTable according to the user input, use an useEffect with an internal state, like so (notice the comments):

"use client";

// ...

const DataTable = <TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) => {
  // ...

  const [internalData, setInternalData] = React.useState(data); // Line I added
  const initialRenderRef= React.useRef(true); // Line I added
  const table = useReactTable({
    columns,
    data: internalData, // Line I changed
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  });

  // Block I added to update the data:
  React.useEffect(() => {
    if(initialRenderRef.current){
      initialRenderRef.current = false;
      return;
    }

    fetch("url") // Use the correct URL, it can be an API Route URL, an external URL...
      .then((res) => res.json())
      .then((data) => setInternalData(data))
      .catch((error) => {
        console.log(error);
      });
  }, [pageSize]); // The fetch will run every time pageSize changes, feel free to add other variables.

  return "...";
};

export default DataTable;

This way, you get the initial data from the server parent component, and then the component fetches the data by itself when the user needs change. That's useful if you worry about SEO, otherwise, you could move even the initial fetch inside the client component itself.

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
  • Hi, that code made me problem as it was not rendering anything. I had to refactor it and just send page and pagelimit as props to the data-table component, and it worked. – steve123 Aug 31 '23 at 09:24