@leancodepl/antd-table-hooks
v10.2.1
Published
Ant Design Table hooks for React
Readme
@leancodepl/antd-table-hooks
React hooks for managing Ant Design table state — sorting, pagination, and filters — with optional URL query parameter persistence via Zod schemas.
Installation
npm install @leancodepl/antd-table-hooksyarn add @leancodepl/antd-table-hooksAPI
useTable(props)
Orchestrates table sorting, pagination, and filter state through URL query parameters. Reads current state from
queryParams and writes updates via setQueryParams, enabling shareable links, browser navigation, and state
restoration on page reload.
Parameters:
| Name | Type | Description |
| ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- |
| props.queryParams | Record<string, unknown> | Current URL search params object (e.g. from TanStack Router's useSearch) |
| props.setQueryParams | (params: Record<string, unknown>) => void | Callback to update URL search params |
| props.definedFilters | FilterDefinition[] | Filter definitions created by defineFilters |
| props.tableId | string | Unique prefix for URL params to avoid collisions between tables on the same route |
| props.defaultSortKey | Key | Initial sort column key |
| props.defaultSortDirection | SortOrder | Initial sort direction ("ascend" or "descend") |
| props.defaultPageSize | number (optional) | Initial page size (defaults to 100) |
Returns: { filters, pagination, sorting } — state objects to pass to useFilters, usePagination, and
useSorting.
useSorting(props)
Manages table column sorting state. Supports two modes: URL-driven (via useTable().sorting) or standalone with local
state.
Parameters (URL-driven mode):
| Name | Type | Description |
| --------------------- | ---------------------------------------------------- | -------------------------- |
| props.sortKey | Key | Current sort column key |
| props.sortDirection | SortOrder | Current sort direction |
| props.onSortUpdate | (sortKey?: Key, sortDirection?: SortOrder) => void | Callback when sort changes |
Parameters (standalone mode):
| Name | Type | Description |
| ---------------------------- | --------------------------------------------------------------- | -------------------------- |
| props.defaultSortKey | Key (optional) | Initial sort column key |
| props.defaultSortDirection | SortOrder (optional) | Initial sort direction |
| props.onSortUpdate | (sortKey?: Key, sortDirection?: SortOrder) => void (optional) | Callback when sort changes |
Returns: { sortData, sortKey, sortDirection, isDescending }
sortData— pass to an Ant Design table'ssortpropsortKey— current sort column keysortDirection— current sort direction ("ascend"|"descend")isDescending—truewhensortDirectionis"descend"
usePagination(props)
Manages table pagination state with zero-indexed page output for API calls. Returns a getTablePagination helper that
produces an Ant Design TablePaginationConfig.
Parameters (URL-driven mode):
| Name | Type | Description |
| -------------------------- | ------------------------------------------------------------ | -------------------------------- |
| props.displayPage | number | Current display page (1-indexed) |
| props.pageSize | number | Current page size |
| props.onPaginationChange | (props: { displayPage: number; pageSize: number }) => void | Callback when pagination changes |
Parameters (standalone mode, all optional):
| Name | Type | Description |
| -------------------------- | ----------------------------------------------------------------------- | -------------------------------------- |
| props.initialDisplayPage | number (optional) | Initial display page (defaults to 1) |
| props.initialPageSize | number (optional) | Initial page size (defaults to 100) |
| props.onPaginationChange | (props: { displayPage: number; pageSize: number }) => void (optional) | Callback when pagination changes |
Returns: { page, pageSize, getTablePagination, resetPage }
page— zero-indexed page number for API callspageSize— current page sizegetTablePagination(total?)— returns an Ant DesignTablePaginationConfigresetPage()— resets to page 1
defineFilters()
Creates a typed filter definition factory along with a combined Zod search schema for URL param validation. Call with a query type generic, then pass an array of filter definitions (or a factory function for context-dependent filters).
Returns: A function that accepts filter definitions and returns { searchSchema, filters }.
searchSchema— Zod schema for filter-related URL params, used withtableSearchSchemafilters— the filter definitions array (or factory function) to pass touseFilters
useFilters(props)
Manages filter state and produces an applyFilters function that applies all active filters to a query object.
Parameters:
| Name | Type | Description |
| ----------------------- | -------------------------------------- | ------------------------------------------------------------- |
| props.filters | FilterDefinition[] | Array of filter definitions (from defineFilters) |
| props.onFiltersChange | (filters, values) => void (optional) | Callback invoked when any filter value changes |
| props.initialValues | Partial<TValues> (optional) | Initial filter values (e.g. from useTable().filters.values) |
Returns: { filters, applyFilters, filterComponents, resetFilters, anyFilterSet }
filters— convenience wrapper object containingapplyFilters,filterComponents,resetFilters, andanyFilterSetapplyFilters(query)— applies all active filter transforms to the query objectfilterComponents— array of React nodes to render filter UIresetFilters()— clears all filtersanyFilterSet—truewhen at least one filter is active
tableSearchSchema(filtersSearchSchema, tableId, sortKeySchema)
Combines filter, pagination, and sorting schemas into a single Zod schema for route validation.
Parameters:
| Name | Type | Description |
| --------------------- | --------------------- | ----------------------------------------------------------- |
| filtersSearchSchema | DefinedSearchSchema | Search schema returned by defineFilters |
| tableId | string | Unique table identifier used to prefix parameter names |
| sortKeySchema | ZodType | Zod schema for the sort key type (e.g. z.coerce.number()) |
Returns: Combined Zod object schema covering all table URL parameters.
buildPaginationSearchSchema(tableId)
Creates a Zod schema for pagination URL parameters ({tableId}-displayPage, {tableId}-pageSize).
Parameters:
| Name | Type | Description |
| --------- | -------- | ------------------------------------------------------ |
| tableId | string | Unique table identifier used to prefix parameter names |
Returns: Zod object schema for pagination params.
buildSortingSearchSchema(tableId, sortKeySchema)
Creates a Zod schema for sorting URL parameters ({tableId}-sortKey, {tableId}-sortDescending).
Parameters:
| Name | Type | Description |
| --------------- | --------- | ----------------------------------------------------------- |
| tableId | string | Unique table identifier used to prefix parameter names |
| sortKeySchema | ZodType | Zod schema for the sort key type (e.g. z.coerce.number()) |
Returns: Zod object schema for sorting params.
Usage Examples
Full Table with URL Query State
The primary pattern — persists sorting, pagination, and filters in URL search params for shareable links and state restoration:
import { useSearch, useNavigate } from "@tanstack/react-router"
import { keepPreviousData } from "@tanstack/react-query"
import {
useTable,
useSorting,
usePagination,
useFilters,
defineFilters,
tableSearchSchema,
InferFiltersSchema,
} from "@leancodepl/antd-table-hooks"
import z from "zod"
const tableId = "reviews"
const { searchSchema: filtersSearchSchema, filters: reviewFilters } = defineFilters<SearchQuery>()([
emailFilter,
statusFilter,
])
export const searchSchema = tableSearchSchema(filtersSearchSchema, tableId, z.coerce.number())
export function ReviewsTable() {
const search = useSearch({ from: "/reviews" })
const navigate = useNavigate({ from: "/reviews" })
const queryState = useTable({
queryParams: search,
setQueryParams: params => navigate({ search: params }),
definedFilters: reviewFilters,
tableId,
defaultSortKey: SortKey.DateCreated,
defaultSortDirection: "descend",
})
const { sortData, sortKey, isDescending } = useSorting(queryState.sorting)
const { page, pageSize, getTablePagination, resetPage } = usePagination(queryState.pagination)
const { applyFilters, filters } = useFilters<SearchQuery, InferFiltersSchema<typeof reviewFilters>>({
filters: reviewFilters,
initialValues: queryState.filters.values,
onFiltersChange: (_, values) => {
queryState.filters.onFiltersChange(values)
resetPage()
},
})
const { data, isPending, isPlaceholderData } = api.useSearchReviews(
applyFilters({ PageNumber: page, PageSize: pageSize, SortBy: sortKey, SortByDescending: isDescending }),
{ placeholderData: keepPreviousData },
)
return (
<Table
columns={columns}
dataSource={data?.items}
loading={isPending || isPlaceholderData}
pagination={getTablePagination(data?.totalCount)}
/>
)
}Route Configuration
Use the combined search schema as validateSearch in a TanStack Router route:
import { createFileRoute } from "@tanstack/react-router"
import { searchSchema } from "./ReviewsTable"
export const Route = createFileRoute("/reviews")({
component: ReviewsPage,
validateSearch: searchSchema,
})Standalone Table (Without URL State)
Use useSorting, usePagination, and useFilters directly when URL persistence is not needed:
import { useSorting, usePagination, useFilters } from "@leancodepl/antd-table-hooks"
import { keepPreviousData } from "@tanstack/react-query"
export function SimpleTable() {
const { sortData, sortKey, isDescending } = useSorting({
defaultSortDirection: "descend",
defaultSortKey: SortKey.CreatedAt,
})
const { page, pageSize, getTablePagination, resetPage } = usePagination()
const { applyFilters, filters } = useFilters({
filters: simpleFilters,
onFiltersChange: resetPage,
})
const { data, isPending } = api.useSearch(
applyFilters({ PageNumber: page, PageSize: pageSize, SortBy: sortKey, SortByDescending: isDescending }),
{ placeholderData: keepPreviousData },
)
return (
<Table
columns={columns}
dataSource={data?.items}
loading={isPending}
pagination={getTablePagination(data?.totalCount)}
/>
)
}Defining Filters
Filters are defined outside the component using defineFilters. Each filter is a FilterDefinition object describing
its ID, component, search schema entries, and how it transforms the query:
import { defineFilters, FilterDefinition } from "@leancodepl/antd-table-hooks"
const tableId = "users"
const { searchSchema: usersFiltersSchema, filters: userFilters } = defineFilters<SearchQuery>()([
emailTextFilter,
stateSelectFilter,
createdAtDateRangeFilter,
])For context-dependent filters (e.g. internationalization):
type FiltersContext = { intl: IntlShape }
const { searchSchema, filters: filtersFn } = defineFilters<SearchQuery, FiltersContext>()(context => [
nameFilter(context?.intl),
])
// In the component:
const intl = useIntl()
const filtersDefinition = useMemo(() => filtersFn({ intl }), [intl])Implementing Custom Filter Definitions
This library does not ship filter UI components — you implement them in your application to match your design system. A
filter definition is an object satisfying the FilterDefinition<TQuery, TValue, TId> type:
| Property | Type | Description |
| ------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| id | string | Unique identifier for the filter within the table |
| component | ComponentType<FilterProps> | React component receiving { applyFilter, reset$, initialValue } |
| searchSchema | [key, ZodType][] (optional) | URL param key/schema pairs for serialization |
| buildApplyFilter | (value) => ((query) => query) \| undefined (optional) | Reconstructs the filter function from a stored value (needed for initial/restored values) |
| toSearchParams | (value) => Record<string, unknown> (required when searchSchema is set) | Serializes the filter value to URL params |
| fromSearchParams | (params) => value \| undefined (required when searchSchema is set) | Deserializes URL params back to a filter value |
Each filter component receives three props from useFilters:
applyFilter(filterFn, value)— call with a query transform function when the value changes, orundefinedto clearreset$— an RxJS Observable that emits when filters are reset; subscribe to clear local stateinitialValue— restored value when hydrating from URL params
Minimal Example
A simple text input filter showing the essential structure:
import { useState, useEffect, useCallback } from "react"
import z from "zod"
import { FilterDefinition } from "@leancodepl/antd-table-hooks"
const schema = z.string().optional().catch(undefined)
export function textFilter<TQuery, const TId extends string, const TTableId extends string>({
id,
tableId,
label,
filter,
}: {
id: TId
tableId: TTableId
label: string
filter: (value: string, query: TQuery) => TQuery
}) {
const paramKey = `${tableId}-${id}` as const
return {
id,
component: ({ applyFilter, reset$, initialValue }) => {
const [value, setValue] = useState(initialValue)
const clear = useCallback(() => {
setValue(undefined)
applyFilter(undefined)
}, [applyFilter])
useEffect(() => {
const sub = reset$.subscribe(clear)
return () => sub.unsubscribe()
}, [clear, reset$])
return (
<input
placeholder={label}
value={value}
onChange={e => {
const v = e.target.value
setValue(v)
applyFilter(v ? query => filter(v, query) : undefined, v)
}}
/>
)
},
buildApplyFilter: value => (value ? query => filter(value, query) : undefined),
searchSchema: [[paramKey, schema]] as const,
toSearchParams: value => ({ [paramKey]: value }),
fromSearchParams: params => params[paramKey] as string | undefined,
} satisfies FilterDefinition<TQuery, string, TId>
}Usage:
textFilter({
id: "email",
tableId: "users",
label: "Email",
filter: (email, query) => ({ ...query, EmailFilter: email }),
})Key Implementation Patterns
Single URL param (text, select): use one searchSchema entry keyed as {tableId}-{id}.
searchSchema: [[`${tableId}-${id}`, z.string().optional()]] as constMultiple URL params (date ranges): use multiple searchSchema entries with distinct keys.
searchSchema: [
[`${tableId}-${id}-from`, z.string().optional()],
[`${tableId}-${id}-to`, z.string().optional()],
] as constClearing: call applyFilter(undefined) when the filter value is empty or cleared. The reset$ observable fires
when the user resets all filters — subscribe to it and clear local state.
Restoring from URL: fromSearchParams deserializes raw URL values back into the filter's value type.
buildApplyFilter reconstructs the query transform function from a deserialized value, enabling filter restoration on
page load.
Reusable Table with Query Params from Parent
Pass query params as props to share the same table component across multiple pages:
import { useSearch, useNavigate } from "@tanstack/react-router"
import z from "zod"
type UsersTableQueryParams = z.infer<typeof usersSearchSchema>
type UsersTableProps = {
name: string
queryParams: UsersTableQueryParams
setQueryParams: (params: UsersTableQueryParams) => void
}
export function UsersTable({ name, queryParams, setQueryParams }: UsersTableProps) {
const queryState = useTable({
queryParams,
setQueryParams,
definedFilters: usersFilters,
tableId: "users",
defaultSortKey: SortKey.CreatedAt,
defaultSortDirection: "descend",
})
// ... same pattern as full table example
}
// Parent page
export function UsersPage() {
const search = useSearch({ from: "/users" })
const navigate = useNavigate({ from: "/users" })
return <UsersTable name="users" queryParams={search} setQueryParams={params => navigate({ search: params })} />
}