- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
Data Table - Admin Components
The DataTable component in Medusa UI allows you to display data in a table with sorting, filtering, and pagination.
You can use this component in your Admin Extensions to display data in a table format, especially if they're retrieved from API routes of the Medusa application.
Example: DataTable with Data Fetching#
In this example, you'll create a UI widget that shows the list of products retrieved from the List Products API Route in a data table with pagination, filtering, searching, and sorting.
Start by initializing the columns in the data table. To do that, use the createDataTableColumnHelper
from Medusa UI:
1import {2 createDataTableColumnHelper,3} from "@medusajs/ui"4import { 5 HttpTypes,6} from "@medusajs/framework/types"7 8const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()9 10const columns = [11 columnHelper.accessor("title", {12 header: "Title",13 // Enables sorting for the column.14 enableSorting: true,15 // If omitted, the header will be used instead if it's a string, 16 // otherwise the accessor key (id) will be used.17 sortLabel: "Title",18 // If omitted the default value will be "A-Z"19 sortAscLabel: "A-Z",20 // If omitted the default value will be "Z-A"21 sortDescLabel: "Z-A",22 }),23 columnHelper.accessor("status", {24 header: "Status",25 cell: ({ getValue }) => {26 const status = getValue()27 return (28 <Badge color={status === "published" ? "green" : "grey"} size="xsmall">29 {status === "published" ? "Published" : "Draft"}30 </Badge>31 )32 },33 }),34]
createDataTableColumnHelper
utility creates a column helper that helps you define the columns for the data table. The column helper has an accessor
method that accepts two parameters:
- The column's key in the table's data.
- An object with the following properties:
header
: The column's header.cell
: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has agetValue
property function to retrieve the raw value of the cell.enableSorting
: (optional) A boolean that enables sorting data by this column.sortLabel
: (optional) The label for the sorting button. If omitted, theheader
will be used instead if it's a string, otherwise the accessor key (id) will be used.sortAscLabel
: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z".sortDescLabel
: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A".
Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status.
To define the filters, add the following:
1// other imports...2import {3 // ...4 createDataTableFilterHelper,5} from "@medusajs/ui"6 7const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()8 9const filters = [10 filterHelper.accessor("status", {11 type: "select",12 label: "Status",13 options: [14 {15 label: "Published",16 value: "published",17 },18 {19 label: "Draft",20 value: "draft",21 },22 ],23 }),24]
createDataTableFilterHelper
utility creates a filter helper that helps you define the filters for the data table. The filter helper has an accessor
method that accepts two parameters:
- The key of a column in the table's data.
- An object with the following properties:
type
: The type of filter. It can be either:select
: A select dropdown allowing users to choose multiple values.radio
: A radio button allowing users to choose one value.date
: A date picker allowing users to choose a date.
label
: The filter's label.options
: An array of objects withlabel
andvalue
properties. Thelabel
is the option's label, and thevalue
is the value to filter by.
You'll now start creating the UI widget's component. Start by adding the necessary state variables:
1// other imports...2import {3 // ...4 DataTablePaginationState,5 DataTableFilteringState,6 DataTableSortingState,7} from "@medusajs/ui"8import { useMemo, useState } from "react"9 10// ...11 12const limit = 1513 14const CustomPage = () => {15 const [pagination, setPagination] = useState<DataTablePaginationState>({16 pageSize: limit,17 pageIndex: 0,18 })19 const [search, setSearch] = useState<string>("")20 const [filtering, setFiltering] = useState<DataTableFilteringState>({})21 const [sorting, setSorting] = useState<DataTableSortingState | null>(null)22 23 const offset = useMemo(() => {24 return pagination.pageIndex * limit25 }, [pagination])26 const statusFilters = useMemo(() => {27 return (filtering.status || []) as ProductStatus28 }, [filtering])29 30 // TODO add data fetching logic31}
In the component, you've added the following state variables:
pagination
: An object of typeDataTablePaginationState
that holds the pagination state. It has two properties:pageSize
: The number of items to show per page.pageIndex
: The current page index.
search
: A string that holds the search query.filtering
: An object of typeDataTableFilteringState
that holds the filtering state.sorting
: An object of typeDataTableSortingState
that holds the sorting state.
You've also added two memoized variables:
offset
: How many items to skip when fetching data based on the current page.statusFilters
: The selected status filters, if any.
Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in this guide, add the following imports at the top of the file:
This imports the JS SDK instance and useQuery
from Tanstack Query.
Then, replace the TODO
in the component with the following:
1const { data, isLoading } = useQuery({2 queryFn: () => sdk.admin.product.list({3 limit,4 offset,5 q: search,6 status: statusFilters,7 order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,8 }),9 queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],10})11 12// TODO configure data table
You use the useQuery
hook to fetch the products from the Medusa application. In the queryFn
, you call the sdk.admin.product.list
method to fetch the products. You pass the following query parameters to the method:
limit
: The number of products to fetch per page.offset
: The number of products to skip based on the current page.q
: The search query, if set.status
: The status filters, if set.order
: The sorting order, if set.
So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.
Next, you'll configure the data table. Medusa UI provides a useDataTable
hook that helps you configure the data table. Add the following imports at the top of the file:
Then, replace the TODO
in the component with the following:
1const table = useDataTable({2 columns,3 data: data?.products || [],4 getRowId: (row) => row.id,5 rowCount: data?.count || 0,6 isLoading,7 pagination: {8 state: pagination,9 onPaginationChange: setPagination,10 },11 search: {12 state: search,13 onSearchChange: setSearch,14 },15 filtering: {16 state: filtering,17 onFilteringChange: setFiltering,18 },19 filters,20 sorting: {21 // Pass the pagination state and updater to the table instance22 state: sorting,23 onSortingChange: setSorting,24 },25})26 27// TODO render component
The useDataTable
hook accepts an object with the following properties:
columns
: The columns to display in the data table. You created this using thecreateDataTableColumnHelper
utility.data
: The products fetched from the Medusa application.getRowId
: A function that returns the unique ID of a row.rowCount
: The total number of products that can be retrieved. This is used to determine the number of pages.isLoading
: A boolean that indicates if the data is being fetched.pagination
: An object to configure pagination. It accepts with the following properties:state
: The pagination React state variable.onPaginationChange
: A function that updates the pagination state.
search
: An object to configure searching. It accepts the following properties:state
: The search query React state variable.onSearchChange
: A function that updates the search query state.
filtering
: An object to configure filtering. It accepts the following properties:state
: The filtering React state variable.onFilteringChange
: A function that updates the filtering state.
filters
: The filters to display in the data table. You created this using thecreateDataTableFilterHelper
utility.sorting
: An object to configure sorting. It accepts the following properties:state
: The sorting React state variable.onSortingChange
: A function that updates the sorting state.
Finally, you'll render the data table. But first, add the following imports at the top of the page:
Aside from the DataTable
component, you also import the SingleColumnLayout and Container components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.
Then, replace the TODO
in the component with the following:
1return (2 <SingleColumnLayout>3 <Container>4 <DataTable instance={table}>5 <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">6 <Heading>Products</Heading>7 <div className="flex gap-2">8 <DataTable.FilterMenu tooltip="Filter" />9 <DataTable.SortingMenu tooltip="Sort" />10 <DataTable.Search placeholder="Search..." />11 </div>12 </DataTable.Toolbar>13 <DataTable.Table />14 <DataTable.Pagination />15 </DataTable>16 </Container>17 </SingleColumnLayout>18)
You render the DataTable
component and pass the table
instance as a prop. In the DataTable
component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.
Lastly, export the component and the UI widget's configuration at the end of the file:
If you start your Medusa application and go to localhost:9000/app/custom
, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.
Full Example Code#
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { ChatBubbleLeftRight } from "@medusajs/icons"3import { 4 Badge,5 createDataTableColumnHelper,6 createDataTableFilterHelper,7 DataTable,8 DataTableFilteringState,9 DataTablePaginationState,10 DataTableSortingState,11 Heading,12 useDataTable,13} from "@medusajs/ui"14import { useQuery } from "@tanstack/react-query"15import { SingleColumnLayout } from "../../layouts/single-column"16import { sdk } from "../../lib/config"17import { useMemo, useState } from "react"18import { Container } from "../../components/container"19import { HttpTypes, ProductStatus } from "@medusajs/framework/types"20 21const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()22 23const columns = [24 columnHelper.accessor("title", {25 header: "Title",26 // Enables sorting for the column.27 enableSorting: true,28 // If omitted, the header will be used instead if it's a string, 29 // otherwise the accessor key (id) will be used.30 sortLabel: "Title",31 // If omitted the default value will be "A-Z"32 sortAscLabel: "A-Z",33 // If omitted the default value will be "Z-A"34 sortDescLabel: "Z-A",35 }),36 columnHelper.accessor("status", {37 header: "Status",38 cell: ({ getValue }) => {39 const status = getValue()40 return (41 <Badge color={status === "published" ? "green" : "grey"} size="xsmall">42 {status === "published" ? "Published" : "Draft"}43 </Badge>44 )45 },46 }),47]48 49const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()50 51const filters = [52 filterHelper.accessor("status", {53 type: "select",54 label: "Status",55 options: [56 {57 label: "Published",58 value: "published",59 },60 {61 label: "Draft",62 value: "draft",63 },64 ],65 }),66]67 68const limit = 1569 70const CustomPage = () => {71 const [pagination, setPagination] = useState<DataTablePaginationState>({72 pageSize: limit,73 pageIndex: 0,74 })75 const [search, setSearch] = useState<string>("")76 const [filtering, setFiltering] = useState<DataTableFilteringState>({})77 const [sorting, setSorting] = useState<DataTableSortingState | null>(null)78 79 const offset = useMemo(() => {80 return pagination.pageIndex * limit81 }, [pagination])82 const statusFilters = useMemo(() => {83 return (filtering.status || []) as ProductStatus84 }, [filtering])85 86 const { data, isLoading } = useQuery({87 queryFn: () => sdk.admin.product.list({88 limit,89 offset,90 q: search,91 status: statusFilters,92 order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,93 }),94 queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],95 })96 97 const table = useDataTable({98 columns,99 data: data?.products || [],100 getRowId: (row) => row.id,101 rowCount: data?.count || 0,102 isLoading,103 pagination: {104 state: pagination,105 onPaginationChange: setPagination,106 },107 search: {108 state: search,109 onSearchChange: setSearch,110 },111 filtering: {112 state: filtering,113 onFilteringChange: setFiltering,114 },115 filters,116 sorting: {117 // Pass the pagination state and updater to the table instance118 state: sorting,119 onSortingChange: setSorting,120 },121 })122 123 return (124 <SingleColumnLayout>125 <Container>126 <DataTable instance={table}>127 <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">128 <Heading>Products</Heading>129 <div className="flex gap-2">130 <DataTable.FilterMenu tooltip="Filter" />131 <DataTable.SortingMenu tooltip="Sort" />132 <DataTable.Search placeholder="Search..." />133 </div>134 </DataTable.Toolbar>135 <DataTable.Table />136 <DataTable.Pagination />137 </DataTable>138 </Container>139 </SingleColumnLayout>140 )141}142 143export const config = defineRouteConfig({144 label: "Custom",145 icon: ChatBubbleLeftRight,146})147 148export default CustomPage