TanStack Table
Refine provides an integration package for TanStack Table library. This package enables you to manage your tables in a headless manner. This adapter supports all of the features of both TanStack Table and Refine's useTable hook (sorting, filtering pagination etc). Simply, you can use any of the TanStack Table examples as-is by copying and pasting them into your project.
Installation
Install the @refinedev/react-table
library.
- npm
- pnpm
- yarn
npm i @refinedev/react-table
pnpm add @refinedev/react-table
yarn add @refinedev/react-table
Usage
Let's see how to display a table with useTable hook.
We provide implementation examples for the Mantine and Chakra UI. If you using a different ui library, you can use the headless example as a starting point.
- Headless
- MantineTanStack Table
- Chakra UITanStack Table
Headless
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@tanstack/react-table@latest,@refinedev/react-table@latest
Content: import React from "react"; import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { ProductTable } from "./product-table.tsx"; const API_URL = "https://api.fake-rest.refine.dev"; export default function App() { return ( <Refine dataProvider={dataProvider(API_URL)}> <ProductTable /> </Refine> ); }
Content: import React from "react"; import { useTable } from "@refinedev/react-table"; import { ColumnDef, flexRender } from "@tanstack/react-table"; export const ProductTable: React.FC = () => { const columns = React.useMemo<ColumnDef<IProduct>[]>( () => [ { id: "id", header: "ID", accessorKey: "id", meta: { filterOperator: "eq", }, }, { id: "name", header: "Name", accessorKey: "name", meta: { filterOperator: "contains", }, }, { id: "price", header: "Price", accessorKey: "price", meta: { filterOperator: "eq", }, }, ], [], ); const { getHeaderGroups, getRowModel, getState, setPageIndex, getCanPreviousPage, getPageCount, getCanNextPage, nextPage, previousPage, setPageSize, } = useTable<IProduct>({ refineCoreProps: { resource: "products", }, columns, }); return ( <div> <h1>Products</h1> <table> <thead> {getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <th key={header.id}> {header.isPlaceholder ? null : ( <> <div onClick={header.column.getToggleSortingHandler()} > {flexRender( header.column.columnDef .header, header.getContext(), )} {{ asc: " 🔼", desc: " 🔽", }[ header.column.getIsSorted() as string ] ?? " ↕️"} </div> </> )} {header.column.getCanFilter() ? ( <div> <input value={ (header.column.getFilterValue() as string) ?? "" } onChange={(e) => header.column.setFilterValue( e.target.value, ) } /> </div> ) : null} </th> ); })} </tr> ))} </thead> <tbody> {getRowModel().rows.map((row) => { return ( <tr key={row.id}> {row.getVisibleCells().map((cell) => { return ( <td key={cell.id}> {flexRender( cell.column.columnDef.cell, cell.getContext(), )} </td> ); })} </tr> ); })} </tbody> </table> <div> <button onClick={() => setPageIndex(0)} disabled={!getCanPreviousPage()} > {"<<"} </button> <button onClick={() => previousPage()} disabled={!getCanPreviousPage()} > {"<"} </button> <button onClick={() => nextPage()} disabled={!getCanNextPage()}> {">"} </button> <button onClick={() => setPageIndex(getPageCount() - 1)} disabled={!getCanNextPage()} > {">>"} </button> <span> Page <strong> {getState().pagination.pageIndex + 1} of{" "} {getPageCount()} </strong> </span> <span> | Go to page: <input type="number" defaultValue={getState().pagination.pageIndex + 1} onChange={(e) => { const page = e.target.value ? Number(e.target.value) - 1 : 0; setPageIndex(page); }} /> </span>{" "} <select value={getState().pagination.pageSize} onChange={(e) => { setPageSize(Number(e.target.value)); }} > {[10, 20, 30, 40, 50].map((pageSize) => ( <option key={pageSize} value={pageSize}> Show {pageSize} </option> ))} </select> </div> </div> ); }; interface IProduct { id: number; name: string; price: string; }
MantineTanStack Table
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/mantine@latest,@refinedev/react-table@latest,@tanstack/react-table@latest,@mantine/core@^5.10.4,@tabler/icons-react@^3.1.0
Content: import React from "react"; import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { MantineProvider, Global } from "@mantine/core"; import { ProductTable } from "./product-table.tsx"; const API_URL = "https://api.fake-rest.refine.dev"; export default function App() { return ( <MantineProvider withNormalizeCSS withGlobalStyles > <Global styles={{ body: { WebkitFontSmoothing: "auto" } }} /> <Refine dataProvider={dataProvider(API_URL)}> <ProductTable /> </Refine> </MantineProvider> ); }
Content: import React from "react"; import { useTable } from "@refinedev/react-table"; import { ColumnDef, flexRender } from "@tanstack/react-table"; import { Box, Group, Table, Pagination } from "@mantine/core"; import { ColumnSorter } from "./column-sorter.tsx"; import { ColumnFilter } from "./column-filter.tsx"; export const ProductTable: React.FC = () => { const columns = React.useMemo<ColumnDef<IProduct>[]>( () => [ { id: "id", header: "ID", accessorKey: "id", meta: { filterOperator: "eq", }, }, { id: "name", header: "Name", accessorKey: "name", meta: { filterOperator: "contains", }, }, { id: "price", header: "Price", accessorKey: "price", meta: { filterOperator: "eq", }, }, ], [], ); const { getHeaderGroups, getRowModel, refineCore: { setCurrent, pageCount, current }, } = useTable({ refineCoreProps: { resource: "products", }, columns, }); return ( <div style={{ padding: "4px" }}> <h2>Products</h2> <Table highlightOnHover> <thead> {getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <th key={header.id}> {!header.isPlaceholder && ( <Group spacing="xs" noWrap> <Box> {flexRender( header.column.columnDef .header, header.getContext(), )} </Box> <Group spacing="xs" noWrap> <ColumnSorter column={header.column} /> <ColumnFilter column={header.column} /> </Group> </Group> )} </th> ); })} </tr> ))} </thead> <tbody> {getRowModel().rows.map((row) => { return ( <tr key={row.id}> {row.getVisibleCells().map((cell) => { return ( <td key={cell.id}> {flexRender( cell.column.columnDef.cell, cell.getContext(), )} </td> ); })} </tr> ); })} </tbody> </Table> <br /> <Pagination position="right" total={pageCount} page={current} onChange={setCurrent} /> </div> ); }; interface IProduct { id: number; name: string; price: string; }
Content: import { ActionIcon } from "@mantine/core"; import { IconChevronDown, IconSelector, IconChevronUp } from "@tabler/icons-react"; export interface ColumnButtonProps { column: Column<any, any>; // eslint-disable-line } export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => { if (!column.getCanSort()) { return null; } const sorted = column.getIsSorted(); return ( <ActionIcon size="xs" onClick={column.getToggleSortingHandler()} style={{ transition: "transform 0.25s", transform: `rotate(${sorted === "asc" ? "180" : "0"}deg)`, }} variant={sorted ? "light" : "transparent"} color={sorted ? "primary" : "gray"} > {!sorted && <IconSelector size={18} />} {sorted === "asc" && <IconChevronDown size={18} />} {sorted === "desc" && <IconChevronUp size={18} />} </ActionIcon> ); };
Content: import React, { useState } from "react"; import { Column } from "@tanstack/react-table"; import { TextInput, Menu, ActionIcon, Stack, Group } from "@mantine/core"; import { IconFilter, IconX, IconCheck } from "@tabler/icons-react"; interface ColumnButtonProps { column: Column<any, any>; // eslint-disable-line } export const ColumnFilter: React.FC<ColumnButtonProps> = ({ column }) => { // eslint-disable-next-line const [state, setState] = useState(null as null | { value: any }); if (!column.getCanFilter()) { return null; } const open = () => setState({ value: column.getFilterValue(), }); const close = () => setState(null); // eslint-disable-next-line const change = (value: any) => setState({ value }); const clear = () => { column.setFilterValue(undefined); close(); }; const save = () => { if (!state) return; column.setFilterValue(state.value); close(); }; const renderFilterElement = () => { // eslint-disable-next-line const FilterComponent = (column.columnDef?.meta as any)?.filterElement; if (!FilterComponent && !!state) { return ( <TextInput autoComplete="off" value={state.value} onChange={(e) => change(e.target.value)} /> ); } return <FilterComponent value={state?.value} onChange={change} />; }; return ( <Menu opened={!!state} position="bottom" withArrow transition="scale-y" shadow="xl" onClose={close} width="256px" withinPortal > <Menu.Target> <ActionIcon size="xs" onClick={open} variant={column.getIsFiltered() ? "light" : "transparent"} color={column.getIsFiltered() ? "primary" : "gray"} > <IconFilter size={18} /> </ActionIcon> </Menu.Target> <Menu.Dropdown> {!!state && ( <Stack p="xs" spacing="xs"> {renderFilterElement()} <Group position="right" spacing={6} noWrap> <ActionIcon size="md" color="gray" variant="outline" onClick={clear} > <IconX size={18} /> </ActionIcon> <ActionIcon size="md" onClick={save} color="primary" variant="outline" > <IconCheck size={18} /> </ActionIcon> </Group> </Stack> )} </Menu.Dropdown> </Menu> ); };
Chakra UITanStack Table
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-table@latest,@tanstack/react-table@latest,@refinedev/chakra-ui@latest,@chakra-ui/react@^2.5.1,@tabler/icons-react@^3.1.0
Content: import React from "react"; import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { ChakraProvider } from "@chakra-ui/react"; import { ProductTable } from "./product-table.tsx"; const API_URL = "https://api.fake-rest.refine.dev"; export default function App() { return ( <ChakraProvider> <Refine dataProvider={dataProvider(API_URL)}> <ProductTable /> </Refine> </ChakraProvider> ); }
Content: import React from "react"; import { useTable } from "@refinedev/react-table"; import { ColumnDef, flexRender } from "@tanstack/react-table"; import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, HStack, Text, } from "@chakra-ui/react"; import { Pagination } from "./pagination"; import { ColumnSorter } from "./column-sorter"; import { ColumnFilter } from "./column-filter"; export const ProductTable: React.FC = () => { const columns = React.useMemo<ColumnDef<IProduct>[]>( () => [ { id: "id", header: "ID", accessorKey: "id", meta: { filterOperator: "eq", }, }, { id: "name", header: "Name", accessorKey: "name", meta: { filterOperator: "contains", }, }, { id: "price", header: "Price", accessorKey: "price", meta: { filterOperator: "eq", }, }, ], [], ); const { getHeaderGroups, getRowModel, refineCore: { setCurrent, pageCount, current }, } = useTable({ refineCoreProps: { resource: "products", }, columns, }); return ( <div style={{ padding:"8px" }}> <Text fontSize='3xl'>Products</Text> <TableContainer whiteSpace="pre-line"> <Table variant="simple"> <Thead> {getHeaderGroups().map((headerGroup) => ( <Tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <Th key={header.id}> {!header.isPlaceholder && ( <HStack spacing="2"> <Text> {flexRender( header.column.columnDef .header, header.getContext(), )} </Text> <HStack spacing="2"> <ColumnSorter column={header.column} /> <ColumnFilter column={header.column} /> </HStack> </HStack> )} </Th> ))} </Tr> ))} </Thead> <Tbody> {getRowModel().rows.map((row) => ( <Tr key={row.id}> {row.getVisibleCells().map((cell) => ( <Td key={cell.id}> {flexRender( cell.column.columnDef.cell, cell.getContext(), )} </Td> ))} </Tr> ))} </Tbody> </Table> </TableContainer> <Pagination current={current} pageCount={pageCount} setCurrent={setCurrent} /> </div> ); }; interface IProduct { id: number; name: string; price: string; }
Content: import { FC } from "react"; import { HStack, Button, Box } from "@chakra-ui/react"; import { usePagination } from "@refinedev/chakra-ui"; export const Pagination: FC<PaginationProps> = ({ current, pageCount, setCurrent, }) => { const pagination = usePagination({ current, pageCount, }); return ( <Box display="flex" justifyContent="flex-end"> <HStack my="3" spacing="1"> {pagination?.prev && ( <Button aria-label="previous page" onClick={() => setCurrent(current - 1)} disabled={!pagination?.prev} variant="outline" > Prev </Button> )} {pagination?.items.map((page) => { if (typeof page === "string") return <span key={page}>...</span>; return ( <Button key={page} onClick={() => setCurrent(page)} variant={page === current ? "solid" : "outline"} > {page} </Button> ); })} {pagination?.next && ( <Button aria-label="next page" onClick={() => setCurrent(current + 1)} variant="outline" > Next </Button> )} </HStack> </Box> ); }; type PaginationProps = { current: number; pageCount: number; setCurrent: (page: number) => void; };
Content: import React, { useState } from "react"; import { IconButton } from "@chakra-ui/react"; import { IconChevronDown, IconChevronUp, IconSelector } from "@tabler/icons-react"; import type { SortDirection } from "@tanstack/react-table"; export interface ColumnButtonProps { column: Column<any, any>; // eslint-disable-line } export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => { if (!column.getCanSort()) { return null; } const sorted = column.getIsSorted(); return ( <IconButton aria-label="Sort" size="xs" onClick={column.getToggleSortingHandler()} icon={<ColumnSorterIcon sorted={sorted} />} variant={sorted ? "light" : "transparent"} color={sorted ? "primary" : "gray"} /> ); }; const ColumnSorterIcon = ({ sorted }: { sorted: false | SortDirection }) => { if (sorted === "asc") return <IconChevronDown size={18} />; if (sorted === "desc") return <IconChevronUp size={18} />; return <IconSelector size={18} />; };
Content: import React, { useState } from "react"; import { Input, Menu, IconButton, MenuButton, MenuList, VStack, HStack, } from "@chakra-ui/react"; import { IconFilter, IconX, IconCheck } from "@tabler/icons-react"; interface ColumnButtonProps { column: Column<any, any>; // eslint-disable-line } export const ColumnFilter: React.FC<ColumnButtonProps> = ({ column }) => { // eslint-disable-next-line const [state, setState] = useState(null as null | { value: any }); if (!column.getCanFilter()) { return null; } const open = () => setState({ value: column.getFilterValue(), }); const close = () => setState(null); // eslint-disable-next-line const change = (value: any) => setState({ value }); const clear = () => { column.setFilterValue(undefined); close(); }; const save = () => { if (!state) return; column.setFilterValue(state.value); close(); }; const renderFilterElement = () => { // eslint-disable-next-line const FilterComponent = (column.columnDef?.meta as any)?.filterElement; if (!FilterComponent && !!state) { return ( <Input borderRadius="md" size="sm" autoComplete="off" value={state.value} onChange={(e) => change(e.target.value)} /> ); } return ( <FilterComponent value={state?.value} onChange={(e: any) => change(e.target.value)} /> ); }; return ( <Menu isOpen={!!state} onClose={close}> <MenuButton onClick={open} as={IconButton} aria-label="Options" icon={<IconFilter size="16" />} variant="ghost" size="xs" /> <MenuList p="2"> {!!state && ( <VStack align="flex-start"> {renderFilterElement()} <HStack spacing="1"> <IconButton aria-label="Clear" size="sm" colorScheme="red" onClick={clear} > <IconX size={18} /> </IconButton> <IconButton aria-label="Save" size="sm" onClick={save} colorScheme="green" > <IconCheck size={18} /> </IconButton> </HStack> </VStack> )} </MenuList> </Menu> ); };