Tables are essential in data-intensive applications, serving as the primary way for organizing and displaying data in a readable format using rows and columns. Their integration, however, is complex due to functionalities like sorting, filtering, and pagination. Refine's tables integration aims to make this process as simple as possible while providing as many real world features as possible out of the box. This guide will cover the basics of tables in Refine and how to use them.
Handling Data useTable
allows us to fetch data according to the sorter, filter, and pagination states. Under the hood, it uses useList
for the fetch. Its designed to be headless, but Refine offers seamless integration with several popular UI libraries, simplifying the use of their table components.
Basic Usage The usage of the useTable
hooks may slightly differ between UI libraries, however, the core functionality of useTable
hook in @refinedev/core
stays consistent in all implementations. The useTable
hook in Refine's core is the foundation of all the other useTable
implementations.
Refine's Core TanStack Table Ant Design Material UI Mantine TanStack Table Chakra UI TanStack Table import React from "react" ;
import { useTable , pageCount , pageSize , current , setCurrent } from "@refinedev/core" ;
export const ProductTable : React.FC = ( ) => {
const { tableQueryResult , pageCount , pageSize , current , setCurrent } = useTable <IProduct>( {
resource : "products" ,
pagination : {
current : 1 ,
pageSize : 10 ,
} ,
} ) ;
const posts = tableQueryResult ?.data ?.data ?? [ ] ;
if ( tableQueryResult ?.isLoading ) {
return < div > Loading...</ div > ;
}
return (
< div style ={ { padding : "8px" } } >
< h1 > Products</ h1 >
< table >
< thead >
< tr >
< th > ID</ th >
< th > Name</ th >
< th > Price</ th >
</ tr >
</ thead >
< tbody >
{ posts .map ( ( post ) => (
< tr key ={ post .id } >
< td > { post .id } </ td >
< td > { post .name } </ td >
< td > { post .price } </ td >
</ tr >
) ) }
</ tbody >
</ table >
< hr />
< p > Current Page: { current } </ p >
< p > Page Size: { pageSize } </ p >
< button
onClick ={ ( ) => {
setCurrent ( current - 1 ) ;
} }
disabled ={ current < 2 }
>
Previous Page
</ button >
< button
onClick ={ ( ) => {
setCurrent ( current + 1 ) ;
} }
disabled ={ current === pageCount }
>
Next Page
</ button >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useTable, pageCount, pageSize, current, setCurrent } from "@refinedev/core";
export const ProductTable: React.FC = () => {
const { tableQueryResult, pageCount, pageSize, current, setCurrent } = useTable<IProduct>({
resource: "products",
pagination: {
current: 1,
pageSize: 10,
},
});
const posts = tableQueryResult?.data?.data ?? [];
if (tableQueryResult?.isLoading) {
return <div>Loading...</div>;
}
return (
<div style={{ padding:"8px" }}>
<h1>Products</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.id}</td>
<td>{post.name}</td>
<td>{post.price}</td>
</tr>
))}
</tbody>
</table>
<hr />
<p>Current Page: {current}</p>
<p>Page Size: {pageSize}</p>
<button
onClick={() => {
setCurrent(current - 1);
}}
disabled={current < 2}
>
Previous Page
</button>
<button
onClick={() => {
setCurrent(current + 1);
}}
disabled={current === pageCount}
>
Next Page
</button>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Check out Refine's useTable
reference page to learn more about the usage and see it in action.
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;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@tanstack/react-table@latest,@refinedev/react-table@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
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;
}
Check out TanStack Table's useTable
reference page to learn more about the usage and see it in action.
import React from "react" ;
import { useTable , FilterDropdown } from "@refinedev/antd" ;
import { Table , Input } from "antd" ;
export const ProductTable : React.FC = ( ) => {
const { tableProps } = useTable <IProduct>( {
resource : "products" ,
filters : {
initial : [
{
field : "name" ,
operator : "contains" ,
value : "" ,
} ,
] ,
} ,
} ) ;
return (
< div style ={ { padding : "4px" } } >
< h2 > Products</ h2 >
< Table { ... tableProps } rowKey ="id" >
< Table .Column
dataIndex ="id"
title ="ID"
sorter ={ { multiple : 2 } }
/>
< Table .Column
dataIndex ="name"
title ="Name"
filterDropdown ={ ( props ) => (
< FilterDropdown { ... props } >
< Input placeholder ="Search by name" />
</ FilterDropdown >
) }
/>
< Table .Column
dataIndex ="price"
title ="Price"
sorter ={ { multiple : 1 } }
/>
</ Table >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
material : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/antd@latest,antd@latest
Code Files File: /App.tsx
Content: import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { ConfigProvider, App as AntdApp } from "antd";
import { ProductTable } from "./product-table.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<ConfigProvider>
<AntdApp>
<Refine dataProvider={dataProvider(API_URL)}>
<ProductTable />
</Refine>
</AntdApp>
</ConfigProvider>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useTable, FilterDropdown } from "@refinedev/antd";
import { Table, Input } from "antd";
export const ProductTable: React.FC = () => {
const { tableProps } = useTable<IProduct>({
resource: "products",
filters: {
initial: [
{
field: "name",
operator: "contains",
value: "",
},
],
},
});
return (
<div style={{ padding: "4px" }}>
<h2>Products</h2>
<Table {...tableProps} rowKey="id">
<Table.Column
dataIndex="id"
title="ID"
sorter={{ multiple: 2 }}
/>
<Table.Column
dataIndex="name"
title="Name"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Input placeholder="Search by name" />
</FilterDropdown>
)}
/>
<Table.Column
dataIndex="price"
title="Price"
sorter={{ multiple: 1 }}
/>
</Table>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
material: string;
}
Check out Ant Design's useTable
reference page to learn more about the usage and see it in action.
import React from "react" ;
import { useDataGrid } from "@refinedev/mui" ;
import { DataGrid , GridColDef } from "@mui/x-data-grid" ;
export const ProductTable : React.FC = ( ) => {
const { dataGridProps } = useDataGrid <IProduct>( {
resource : "products" ,
} ) ;
const columns = React .useMemo <GridColDef<IProduct>[ ] >(
( ) => [
{
field : "id" ,
headerName : "ID" ,
type : "number" ,
width : 50 ,
} ,
{ field : "name" , headerName : "Name" , minWidth : 400 , flex : 1 } ,
{ field : "price" , headerName : "Price" , minWidth : 120 , flex : 0.3 } ,
] ,
[ ] ,
) ;
return (
< div style ={ { padding : "4px" } } >
< h2 > Products</ h2 >
< DataGrid { ... dataGridProps } columns ={ columns } />
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/mui@latest,@mui/x-data-grid@latest,@mui/material@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useDataGrid } from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
export const ProductTable: React.FC = () => {
const { dataGridProps } = useDataGrid<IProduct>({
resource: "products",
});
const columns = React.useMemo<GridColDef<IProduct>[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
width: 50,
},
{ field: "name", headerName: "Name", minWidth: 400, flex: 1 },
{ field: "price", headerName: "Price", minWidth: 120, flex: 0.3 },
],
[],
);
return (
<div style={{ padding:"4px" }}>
<h2>Products</h2>
<DataGrid {...dataGridProps} columns={columns} />
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Check out Material UI's useDataGrid
reference page to learn more about the usage and see it in action.
App.tsx product-table.tsx column-sorter.tsx column-filter.tsx
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;
}
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@^1.119.0
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
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;
}
File: /column-sorter.tsx
Content: import { ActionIcon } from "@mantine/core";
import { IconChevronDown, IconSelector, IconChevronUp } from "@tabler/icons";
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>
);
};
File: /column-filter.tsx
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";
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>
);
};
Check out TanStack Table's useTable
reference page to learn more about the usage and see it in action.
App.tsx product-table.tsx pagination.tsx column-sorter.tsx column-filter.tsx
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;
}
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@^1.119.0
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
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;
}
File: /pagination.tsx
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;
};
File: /column-sorter.tsx
Content: import React, { useState } from "react";
import { IconButton } from "@chakra-ui/react";
import { IconChevronDown, IconChevronUp, IconSelector } from "@tabler/icons";
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} />;
};
File: /column-filter.tsx
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";
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>
);
};
Check out TanStack Table's useTable
reference page to learn more about the usage and see it in action.
useTable
has a pagination feature. The pagination is done by passing the current
, pageSize
and, mode
keys to pagination
object.
current : The page index.pageSize : The number of items per page.mode : Whether to use server side pagination or not.When server
is selected, the pagination will be handled on the server side. When client
is selected, the pagination will be handled on the client side. No request will be sent to the server. When off
is selected, the pagination will be disabled. All data will be fetched from the server. You can also change the current
and pageSize
values by using the setCurrent
and setPageSize
functions that are returned by the useTable
hook. Every change will trigger a new fetch.
import React from "react" ;
import { useTable } from "@refinedev/core" ;
export const ProductTable : React.FC = ( ) => {
const { tableQueryResult , pageCount , pageSize , current , setCurrent } = useTable <IProduct>( {
resource : "products" ,
pagination : {
current : 1 ,
pageSize : 10 ,
mode : "server" ,
} ,
} ) ;
const posts = tableQueryResult ?.data ?.data ?? [ ] ;
if ( tableQueryResult ?.isLoading ) {
return < div > Loading...</ div > ;
}
return (
< div >
< h1 > Products</ h1 >
< table >
< thead >
< tr >
< th > ID</ th >
< th > Name</ th >
< th > Price</ th >
</ tr >
</ thead >
< tbody >
{ posts .map ( ( post ) => (
< tr key ={ post .id } >
< td > { post .id } </ td >
< td > { post .name } </ td >
< td > { post .price } </ td >
</ tr >
) ) }
</ tbody >
</ table >
< hr />
< p > Current Page: { current } </ p >
< p > Page Size: { pageSize } </ p >
< button
onClick ={ ( ) => {
setCurrent ( current - 1 ) ;
} }
disabled ={ current < 2 }
>
Previous Page
</ button >
< button
onClick ={ ( ) => {
setCurrent ( current + 1 ) ;
} }
disabled ={ current === pageCount }
>
Next Page
</ button >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useTable } from "@refinedev/core";
export const ProductTable: React.FC = () => {
const { tableQueryResult, pageCount, pageSize, current, setCurrent } = useTable<IProduct>({
resource: "products",
pagination: {
current: 1,
pageSize: 10,
mode: "server", // "client" or "server"
},
});
const posts = tableQueryResult?.data?.data ?? [];
if (tableQueryResult?.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Products</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.id}</td>
<td>{post.name}</td>
<td>{post.price}</td>
</tr>
))}
</tbody>
</table>
<hr />
<p>Current Page: {current}</p>
<p>Page Size: {pageSize}</p>
<button
onClick={() => {
setCurrent(current - 1);
}}
disabled={current < 2}
>
Previous Page
</button>
<button
onClick={() => {
setCurrent(current + 1);
}}
disabled={current === pageCount}
>
Next Page
</button>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Filtering useTable
has a filter feature. The filter is done by using the initial
, permanent
, defaultBehavior
and mode
keys to filters
object.
These states are a CrudFilters
type for creating complex single or multiple queries.
initial : The initial filter state. It can be changed by the setFilters
function.permanent : The default and unchangeable filter state. It can't be changed by the setFilters
function.defaultBehavior : The default behavior of the setFilters
function.When merge
is selected, the new filters will be merged with the old ones. When replace
is selected, the new filters will replace the old ones. It means that the old filters will be deleted. mode : Whether to use server side filter or not.When server
is selected, the filters will be sent to the server. When off
is selected, the filters will be applied on the client side. useTable
will pass these states to dataProvider
for making it possible to fetch the data you need. Handling and adapting these states for API requests is the responsibility of the dataProvider
import React from "react" ;
import { useTable } from "@refinedev/core" ;
export const ProductTable : React.FC = ( ) => {
const { tableQueryResult , filters , setFilters } = useTable <IProduct>( {
resource : "products" ,
filters : {
permanent : [
{
field : "price" ,
value : "200" ,
operator : "lte" ,
} ,
] ,
initial : [ { field : "category.id" , operator : "eq" , value : "1" } ] ,
} ,
} ) ;
const products = tableQueryResult ?.data ?.data ?? [ ] ;
const getFilterByField = ( field : string) => {
return filters .find ( ( filter ) => {
if ( "field" in filter && filter .field === field ) {
return filter ;
}
} ) as LogicalFilter | undefined;
} ;
const resetFilters = ( ) => {
setFilters ( [ ] , "replace" ) ;
} ;
if ( tableQueryResult .isLoading ) {
return < div > Loading...</ div > ;
}
return (
< div >
< h1 > Products with price less than 200</ h1 >
< table >
< thead >
< tr >
< th > ID</ th >
< th > Name</ th >
< th > Price</ th >
< th > categoryId</ th >
</ tr >
</ thead >
< tbody >
{ products .map ( ( product ) => (
< tr key ={ product .id } >
< td > { product .id } </ td >
< td > { product .name } </ td >
< td > { product .price } </ td >
< td > { product .category .id } </ td >
</ tr >
) ) }
</ tbody >
</ table >
< hr />
Filtering by field:
< b >
{ getFilterByField ( "category.id" ) ?.field } , operator{ " " }
{ getFilterByField ( "category.id" ) ?.operator } , value
{ getFilterByField ( "category.id" ) ?.value }
</ b >
< br />
< button
onClick ={ ( ) => {
setFilters ( [
{
field : "category.id" ,
operator : "eq" ,
value :
getFilterByField ( "category.id" ) ?.value === "1"
? "2"
: "1" ,
} ,
] ) ;
} }
>
Toggle Filter
</ button >
< button onClick ={ resetFilters } > Reset filter</ button >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
category : {
id : number;
} ;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useTable } from "@refinedev/core";
export const ProductTable: React.FC = () => {
const { tableQueryResult, filters, setFilters } = useTable<IProduct>({
resource: "products",
filters: {
permanent: [
{
field: "price",
value: "200",
operator: "lte",
},
],
initial: [{ field: "category.id", operator: "eq", value: "1" }],
},
});
const products = tableQueryResult?.data?.data ?? [];
const getFilterByField = (field: string) => {
return filters.find((filter) => {
if ("field" in filter && filter.field === field) {
return filter;
}
}) as LogicalFilter | undefined;
};
const resetFilters = () => {
setFilters([], "replace");
};
if (tableQueryResult.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Products with price less than 200</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>categoryId</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.name}</td>
<td>{product.price}</td>
<td>{product.category.id}</td>
</tr>
))}
</tbody>
</table>
<hr />
Filtering by field:
<b>
{getFilterByField("category.id")?.field}, operator{" "}
{getFilterByField("category.id")?.operator}, value
{getFilterByField("category.id")?.value}
</b>
<br />
<button
onClick={() => {
setFilters([
{
field: "category.id",
operator: "eq",
value:
getFilterByField("category.id")?.value === "1"
? "2"
: "1",
},
]);
}}
>
Toggle Filter
</button>
<button onClick={resetFilters}>Reset filter</button>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
category: {
id: number;
};
}
Sorting useTable
has a sorter feature. The sorter is done by passing the initial
and permanent
keys to sorters
object. These states are a CrudSorter
type for creating single or multiple queries.
initial : The initial sorter state. It can be changed by the setSorters
function.permanent : The default and unchangeable sorter state. It can't be changed by the setSorters
function.useTable
will pass these states to dataProvider
for making it possible to fetch the data you need. Handling and adapting these states for API requests is the responsibility of the dataProvider
You can change the sorters state by using the setSorters
function. Every change will trigger a new fetch.
import React from "react" ;
import { useTable } from "@refinedev/core" ;
export const ProductTable : React.FC = ( ) => {
const { tableQueryResult , sorters , setSorters } = useTable <IProduct>( {
resource : "products" ,
sorters : {
initial : [ { field : "price" , order : "asc" } ] ,
} ,
} ) ;
const products = tableQueryResult ?.data ?.data ?? [ ] ;
const findSorterByFieldName = ( fieldName : string) => {
return sorters .find ( ( sorter ) => sorter .field === fieldName ) ;
} ;
if ( tableQueryResult .isLoading ) {
return < div > Loading...</ div > ;
}
return (
< div >
< h1 > Products</ h1 >
< table >
< thead >
< tr >
< th > ID</ th >
< th > Name</ th >
< th > Price</ th >
</ tr >
</ thead >
< tbody >
{ products .map ( ( product ) => (
< tr key ={ product .id } >
< td > { product .id } </ td >
< td > { product .name } </ td >
< td > { product .price } </ td >
</ tr >
) ) }
</ tbody >
</ table >
< hr />
< hr />
Sorting by field:
< b >
{ findSorterByFieldName ( "price" ) ?.field } , order{ " " }
{ findSorterByFieldName ( "price" ) ?.order }
</ b >
< br />
< button
onClick ={ ( ) => {
setSorters ( [
{
field : "price" ,
order :
findSorterByFieldName ( "price" ) ?.order === "asc"
? "desc"
: "asc" ,
} ,
] ) ;
} }
>
Toggle Sort
</ button >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useTable } from "@refinedev/core";
export const ProductTable: React.FC = () => {
const { tableQueryResult, sorters, setSorters } = useTable<IProduct>({
resource: "products",
sorters: {
initial: [{ field: "price", order: "asc" }],
},
});
const products = tableQueryResult?.data?.data ?? [];
const findSorterByFieldName = (fieldName: string) => {
return sorters.find((sorter) => sorter.field === fieldName);
};
if (tableQueryResult.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Products</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.name}</td>
<td>{product.price}</td>
</tr>
))}
</tbody>
</table>
<hr />
<hr />
Sorting by field:
<b>
{findSorterByFieldName("price")?.field}, order{" "}
{findSorterByFieldName("price")?.order}
</b>
<br />
<button
onClick={() => {
setSorters([
{
field: "price",
order:
findSorterByFieldName("price")?.order === "asc"
? "desc"
: "asc",
},
]);
}}
>
Toggle Sort
</button>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Search useTable
has a search feature with onSearch
. The search is done by using the onSearch
function with searchFormProps
. These feature enables you to easily connect form state to the table filters.
onSearch : function is triggered when the searchFormProps.onFinish
is called. It receives the form values as the first argument and expects a promise that returns a CrudFilters
type.searchFormProps : Has necessary props for the <form>
.For example we can fetch product with the name that contains the search value.
import React from "react" ;
import { HttpError } from "@refinedev/core" ;
import { useTable } from "@refinedev/antd" ;
import { Button , Form , Input , Space , Table } from "antd" ;
export const ProductTable : React.FC = ( ) => {
const { tableProps , searchFormProps } = useTable <
IProduct,
HttpError,
IProduct
>( {
resource : "products" ,
onSearch : ( values ) => {
return [
{
field : "name" ,
operator : "contains" ,
value : values .name ,
} ,
] ;
} ,
} ) ;
return (
< div style ={ { padding : "4px" } } >
< h2 > Products</ h2 >
< Form { ... searchFormProps } >
< Space >
< Form .Item name ="name" >
< Input placeholder ="Search by name" />
</ Form .Item >
< Form .Item >
< Button htmlType ="submit" > Search</ Button >
</ Form .Item >
</ Space >
</ Form >
< Table { ... tableProps } rowKey ="id" >
< Table .Column dataIndex ="id" title ="ID" />
< Table .Column dataIndex ="name" title ="Name" />
< Table .Column dataIndex ="price" title ="Price" />
</ Table >
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/antd@latest,antd@latest
Code Files File: /App.tsx
Content: import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { ConfigProvider, App as AntdApp } from "antd";
import { ProductTable } from "./product-table.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<ConfigProvider>
<AntdApp>
<Refine dataProvider={dataProvider(API_URL)}>
<ProductTable />
</Refine>
</AntdApp>
</ConfigProvider>
);
}
File: /product-table.tsx
Content: import React from "react";
import { HttpError } from "@refinedev/core";
import { useTable } from "@refinedev/antd";
import { Button, Form, Input, Space, Table } from "antd";
export const ProductTable: React.FC = () => {
const { tableProps, searchFormProps } = useTable<
IProduct,
HttpError,
IProduct
>({
resource: "products",
onSearch: (values) => {
return [
{
field: "name",
operator: "contains",
value: values.name,
},
];
},
});
return (
<div style={{ padding: "4px" }}>
<h2>Products</h2>
<Form {...searchFormProps}>
<Space>
<Form.Item name="name">
<Input placeholder="Search by name" />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Search</Button>
</Form.Item>
</Space>
</Form>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="name" title="Name" />
<Table.Column dataIndex="price" title="Price" />
</Table>
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Check out Ant Design's useTable
reference page to learn more about the usage and see it in action.
import React from "react" ;
import { useDataGrid } from "@refinedev/mui" ;
import { DataGrid , GridColDef } from "@mui/x-data-grid" ;
import { HttpError } from "@refinedev/core" ;
import Typography from "@mui/material/Typography" ;
import Box from "@mui/material/Box" ;
import Button from "@mui/material/Button" ;
import Input from "@mui/material/Input" ;
export const ProductTable : React.FC = ( ) => {
const { dataGridProps , search } = useDataGrid <
IProduct,
HttpError,
Partial<IProduct>
>( {
onSearch : ( values ) => {
return [
{
field : "name" ,
operator : "contains" ,
value : values .name ,
} ,
] ;
} ,
resource : "products" ,
} ) ;
const columns = React .useMemo <GridColDef<IProduct>[ ] >(
( ) => [
{
field : "id" ,
headerName : "ID" ,
type : "number" ,
width : 50 ,
} ,
{ field : "name" , headerName : "Name" , minWidth : 400 , flex : 1 } ,
{ field : "price" , headerName : "Price" , minWidth : 120 , flex : 0.3 } ,
] ,
[ ] ,
) ;
return (
< div style ={ { padding : "4px" } } >
< Typography variant ="h4" component ="h2" >
Products
</ Typography >
< Box sx ={ { mt : 2 } } >
< form
onSubmit ={ ( e ) => {
e .preventDefault ( ) ;
const target = e .target as typeof e .target & {
name : { value : string } ;
} ;
search ( { name : target .name .value } ) ;
} }
>
< Input placeholder ="Search by name" name ="name" />
< Button type ="submit" > Search</ Button >
</ form >
</ Box >
< DataGrid { ... dataGridProps } columns ={ columns } sx ={ { mt : 2 } } />
</ div >
) ;
} ;
interface IProduct {
id : number;
name : string;
price : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/mui@latest,@mui/x-data-grid@latest,@mui/material@latest
Code Files File: /App.tsx
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>
);
}
File: /product-table.tsx
Content: import React from "react";
import { useDataGrid } from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { HttpError } from "@refinedev/core";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Input from "@mui/material/Input";
export const ProductTable: React.FC = () => {
const { dataGridProps, search } = useDataGrid<
IProduct,
HttpError,
Partial<IProduct>
>({
onSearch: (values) => {
return [
{
field: "name",
operator: "contains",
value: values.name,
},
];
},
resource: "products",
});
const columns = React.useMemo<GridColDef<IProduct>[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
width: 50,
},
{ field: "name", headerName: "Name", minWidth: 400, flex: 1 },
{ field: "price", headerName: "Price", minWidth: 120, flex: 0.3 },
],
[],
);
return (
<div style={{ padding: "4px" }}>
<Typography variant="h4" component="h2">
Products
</Typography>
<Box sx={{ mt: 2 }}>
<form
onSubmit={(e) => {
e.preventDefault();
const target = e.target as typeof e.target & {
name: { value: string };
};
search({ name: target.name.value });
}}
>
<Input placeholder="Search by name" name="name" />
<Button type="submit">Search</Button>
</form>
</Box>
<DataGrid {...dataGridProps} columns={columns} sx={{ mt: 2 }} />
</div>
);
};
interface IProduct {
id: number;
name: string;
price: string;
}
Check out Material UI's useDataGrid
reference page to learn more about the usage and see it in action.
Integrating with Routers Resource useTable
can infer current resource
from the current route based on your resource definitions. This eliminates the need of passing these parameters to the hooks manually.
useTable ( { resource : "products" , } ) ;
Sync with Location When you use the syncWithLocation
feature, the useTable
's state (e.g., sort order, filters, pagination) is automatically encoded in the query parameters of the URL, and when the URL changes, the useTable
state is automatically updated to match. This makes it easy to share table state across different routes or pages, and to allow users to bookmark or share links to specific table views.
Relationships Refine handles data relations with data hooks(eg: useOne
, useMany
, etc.). This compositional design allows you to flexibly and efficiently manage data relationships to suit your specific requirements.
For example imagine each post has a many category. We can fetch the categories of the post by using the useMany
hook.
import React from "react" ;
import { useTable , HttpError , useMany } from "@refinedev/core" ;
export const HomePage : React.FC = ( ) => {
const { tableQueryResult } = useTable <IPost, HttpError>( {
resource : "posts" ,
} ) ;
const posts = tableQueryResult ?.data ?.data ?? [ ] ;
const categoryIds = posts .map ( ( item ) => item .category .id ) ;
const { data : categoriesData , isLoading } = useMany <ICategory>( {
resource : "categories" ,
ids : categoryIds ,
queryOptions : {
enabled : categoryIds .length > 0 ,
} ,
} ) ;
if ( tableQueryResult ?.isLoading ) {
return < div > Loading...</ div > ;
}
return (
< div >
< h1 > Posts</ h1 >
< table >
< thead >
< tr >
< th > ID</ th >
< th > Title</ th >
< th > Category</ th >
</ tr >
</ thead >
< tbody >
{ posts .map ( ( post ) => (
< tr key ={ post .id } >
< td > { post .id } </ td >
< td > { post .title } </ td >
< td >
{ isLoading ? (
< div > Loading...</ div >
) : (
categoriesData ?.data .find (
( item ) => item .id === post .category .id ,
) ?.title
) }
</ td >
</ tr >
) ) }
</ tbody >
</ table >
</ div >
) ;
} ;
interface IPost {
id : number;
title : string;
category : {
id : number;
} ;
}
interface ICategory {
id : number;
title : string;
}
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest
Code Files File: /App.tsx
Content: import React from "react";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import { HomePage } from "./home-page.tsx";
const API_URL = "https://api.fake-rest.refine.dev";
export default function App() {
return (
<Refine
dataProvider={dataProvider(API_URL)}
>
<HomePage />
</Refine>
);
}
File: /home-page.tsx
Content: import React from "react";
import { useTable, HttpError, useMany } from "@refinedev/core";
export const HomePage: React.FC = () => {
const { tableQueryResult } = useTable<IPost, HttpError>({
resource: "posts",
});
const posts = tableQueryResult?.data?.data ?? [];
const categoryIds = posts.map((item) => item.category.id);
const { data: categoriesData, isLoading } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});
if (tableQueryResult?.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Category</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.id}</td>
<td>{post.title}</td>
<td>
{isLoading ? (
<div>Loading...</div>
) : (
categoriesData?.data.find(
(item) => item.id === post.category.id,
)?.title
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
interface IPost {
id: number;
title: string;
category: {
id: number;
};
}
interface ICategory {
id: number;
title: string;
}