Skip to content

Library Utilities

Utility functions, constants, and helper functions for the data table.

The lib/ directory contains utility functions, constants, and helper functions used throughout the data table components.

Centralized constants for join operators, filter operators, filter variants, and UI configuration.

Join operators for combining multiple filters.

Constant Value Description
AND "and" Logical AND (all filters must match)
OR "or" Logical OR (any filter can match)
MIXED "mixed" Mixed logic (combination of AND/OR)

Filter operator constants defining the comparison logic. Naming follows SQL/PostgREST standards.

Constant Value Description
ILIKE "ilike" SQL ILIKE (Case-insensitive search)
NOT_ILIKE "not.ilike" SQL NOT ILIKE
EQ "eq" SQL EQUAL (=)
NEQ "neq" SQL NOT EQUAL (!=)
IN "in" SQL IN (one of)
NOT_IN "not.in" SQL NOT IN (none of)
EMPTY "empty" Value is null or empty string
NOT_EMPTY "not.empty" Value is not null and not empty string
LT "lt" SQL LESS THAN (<)
LTE "lte" SQL LESS THAN OR EQUAL (<=)
GT "gt" SQL GREATER THAN (>)
GTE "gte" SQL GREATER THAN OR EQUAL (>=)
BETWEEN "between" SQL BETWEEN (range)
RELATIVE "relative" Relative date calculation (e.g., “today”)

Filter variant constants defining the UI control type.

Constant Value Description
TEXT "text" Standard text input
NUMBER "number" Numeric input
RANGE "range" Two-value range input
DATE "date" Single date picker
DATE_RANGE "dateRange" Date range picker
SELECT "select" Single select dropdown
MULTI_SELECT "multiSelect" Multi-select dropdown
BOOLEAN "boolean" Checkbox or toggle

Global default values.

Property Value Description
JOIN_OPERATOR JOIN_OPERATORS.AND Default join operator
PAGE_SIZE 10 Default page size
PAGE_INDEX 0 Default page index

UI-related constraints and settings.

Property Value Description
FILTER_ID_MAX_LENGTH 100 Max characters allowed for a filter ID
MAX_FILTER_DISPLAY_HEIGHT 300 Default max height for scrollable filter popovers
DEBOUNCE_DELAY 300 Default debounce delay in milliseconds for search inputs

Default keyboard shortcut key mappings.

Property Value Description
FILTER_TOGGLE "f" Open/Toggle filter menu
FILTER_REMOVE "f" Remove active filter (with Shift)
ESCAPE "escape" Close active UI elements
ENTER "enter" Confirm or submit active action
BACKSPACE "backspace" Remove character or navigate back
DELETE "delete" Item deletion

Helper functions for filter operations and validation.

Get available filter operators for a given filter variant.

import { getFilterOperators } from "@/components/niko-table/lib/data-table"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
const textOperators = getFilterOperators(FILTER_VARIANTS.TEXT)
// Returns: [{ label: "Contains", value: "ilike" }, ...]
Name Type Description
filterVariant FilterVariant The filter variant type

Array of operator objects with label and value properties.

Type: Array<{ label: string; value: FilterOperator }>

Get the default filter operator for a given filter variant.

import { getDefaultFilterOperator } from "@/components/niko-table/lib/data-table"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
const defaultOp = getDefaultFilterOperator(FILTER_VARIANTS.TEXT)
// Returns: "ilike"
Name Type Description
filterVariant FilterVariant The filter variant type

FilterOperator - The default operator for the variant.

Filter out invalid filters from an array.

import { getValidFilters } from "@/components/niko-table/lib/data-table"
const validFilters = getValidFilters(filters)
// Returns: ExtendedColumnFilter<TData>[] with only valid filters
Name Type Description
filters ExtendedColumnFilter<TData>[] Array of filters to validate

ExtendedColumnFilter<TData>[] - Array containing only valid filters.

Formatting functions for dates, labels, and query strings.

Format a date value into a human-readable string.

import { formatDate } from "@/components/niko-table/lib/format"
formatDate(new Date())
// Returns: "January 1, 2024"
formatDate(new Date(), { month: "short", day: "numeric" })
// Returns: "Jan 1"
Name Type Default Description
date Date | string | number | undefined - Date value to format
opts Intl.DateTimeFormatOptions {} Formatting options

string - Formatted date string, or empty string if date is invalid.

Format a value into a human-readable label. Capitalizes first letter of each word and replaces hyphens/underscores with spaces.

import { formatLabel } from "@/components/niko-table/lib/format"
formatLabel("firstName") // "FirstName"
formatLabel("first-name") // "First Name"
formatLabel("first_name") // "First Name"
formatLabel("true") // "Yes"
formatLabel("false") // "No"
Name Type Description
value string Value to format

string - Formatted label string.

Create a date relative to the current date by subtracting days.

import { daysAgo } from "@/components/niko-table/lib/format"
daysAgo(7) // 7 days ago
daysAgo(30) // 30 days ago (1 month)
daysAgo(365) // 365 days ago (1 year)
Name Type Description
days number Number of days to subtract

Date - Date object representing the date N days ago.

Format URL query parameters into a human-readable query string for display.

import { formatQueryString } from "@/components/niko-table/lib/format"
const urlParams = { search: "i", globalFilter: { filters: [...], joinOperator: "mixed" } }
const urlKeys = { search: "search", globalFilter: "global" }
formatQueryString(urlParams, urlKeys)
// Returns: "?search=i&global={"filters":[...], "joinOperator":"mixed"}"
Name Type Description
urlParams Record<string, unknown> The parsed URL parameters object
urlKeys Record<string, string> Mapping of parameter keys to URL query keys

string - Formatted query string (e.g., ?search=i&global={"filters":[...]}).

Custom filter functions for TanStack Table with performance optimizations.

The filter functions module provides optimized filter implementations for TanStack Table. It includes:

  • Regex caching - Caches compiled regex patterns for 70-90% faster filter execution
  • Operator support - Implements all filter operators (ilike, eq, neq, in, between, etc.)
  • Performance optimizations - LRU-style cache eviction to prevent memory leaks

The filter functions use regex caching to improve performance:

  • Without caching: 1,000 rows × 10 columns = 10,000 regex creations per search keystroke (~100-500ms)
  • With caching: First search creates regex once, subsequent searches reuse cached regex (~5-20ms)

The cache has a maximum size of 100 entries and uses LRU-like eviction to prevent memory leaks.

Custom filter function for TanStack Table that supports all filter operators (ilike, eq, neq, in, not_in, between, lt, gt, lte, gte, empty, not_empty). Used as the default filterFn for columns with advanced filtering.

import { extendedFilter } from "@/components/niko-table/lib/filter-functions"
const columns = [
{
accessorKey: "name",
filterFn: extendedFilter,
},
]

Custom global filter function that searches across all visible columns. Supports debounced search input.

import { globalFilter } from "@/components/niko-table/lib/filter-functions"
const table = useReactTable({
globalFilterFn: globalFilter,
})

Filter function for numeric range columns. Filters rows where the value is between a [min, max] range.

import { numberRangeFilter } from "@/components/niko-table/lib/filter-functions"
const columns = [
{
accessorKey: "price",
filterFn: numberRangeFilter,
},
]

Filter function for date range columns. Filters rows where the date falls between a start and end date.

import { dateRangeFilter } from "@/components/niko-table/lib/filter-functions"
const columns = [
{
accessorKey: "createdAt",
filterFn: dateRangeFilter,
},
]

Utility to create a properly typed filter value object from an ExtendedColumnFilter. Used internally by the filter menu and inline filter components.

import { createFilterValue } from "@/components/niko-table/lib/filter-functions"
const filterValue = createFilterValue(filter)

Processes an array of ExtendedColumnFilter objects and decides how they should be routed: pure-AND rule sets go to TanStack’s columnFilters; anything with OR (or mixed) join logic must ride in globalFilter instead, since TanStack combines column filters with AND only.

import { processFiltersForLogic } from "@/components/niko-table/lib/data-table"
const {
processedFilters, // filters with normalized joinOperator values
hasOrFilters, // any rule joined with OR
hasSameColumnFilters, // multiple rules target one column
shouldUseGlobalFilter, // true → route via globalFilter, not columnFilters
joinOperator, // "and" | "mixed"
} = processFiltersForLogic(filters)

Helper functions for styling table components, particularly for column pinning.

Returns CSS styles for pinned columns (sticky positioning, z-index, shadows). Used internally by DataTable components, but available for custom implementations.

import { getCommonPinningStyles } from "@/components/niko-table/lib/styles"
// In a custom header cell
const pinningStyles = getCommonPinningStyles(column, true)
// In a custom body cell
const pinningStyles = getCommonPinningStyles(column, false)
Name Type Default Description
column Column<TData> - TanStack Table column instance
isHeader boolean false Whether this is a header cell (sets top: 0 for vertical stickiness and z-index 20)

React.CSSProperties - CSS styles object with sticky positioning, z-index, background, and shadow.

Style Properties Applied:

  • position: sticky - Keeps column fixed during horizontal scroll
  • top - Set to 0 for header cells so they stick when the table scrolls vertically (sticky header)
  • left or right - Calculated position based on pinning side
  • zIndex - 20 for headers, 10 for body cells
  • backgroundColor - Uses var(--background) for opaque overlay
  • boxShadow - Visual separator using var(--border) for pinned column edges

Shared scroll-listener body used by every data-table body component (DataTableBody, DataTableVirtualizedBody, DataTableVirtualizedDndBody, DataTableVirtualizedDndColumnBody). Extracted so scroll math changes propagate to all four bodies and so consumers writing custom bodies can reuse the canonical implementation.

Builds a passive-listener-friendly scroll handler that reads container metrics on each scroll event and dispatches:

  • onScroll with a ScrollEvent snapshot (scrollTop, scrollHeight, clientHeight, isTop, isBottom, percentage)
  • onScrolledTop when scrollTop === 0
  • onScrolledBottom when within scrollThreshold pixels of the end

Returned listener is intended to be attached with addEventListener("scroll", handler, { passive: true }) so the browser can keep scroll on its compositor thread (smoother on mobile and during virtual scroll).

import { createScrollHandler } from "@/components/niko-table/lib/create-scroll-handler"
React.useEffect(() => {
const container = containerRef.current
if (!container) return
if (!onScroll && !onScrolledTop && !onScrolledBottom) return
const handleScroll = createScrollHandler({
onScroll,
onScrolledTop,
onScrolledBottom,
scrollThreshold: 50,
})
container.addEventListener("scroll", handleScroll, { passive: true })
return () => container.removeEventListener("scroll", handleScroll)
}, [onScroll, onScrolledTop, onScrolledBottom])

Single options object:

Name Type Default Description
onScroll (event: ScrollEvent) => void (optional) - Called on every scroll event with a snapshot of container metrics
onScrolledTop () => void (optional) - Called when scrollTop === 0
onScrolledBottom () => void (optional) - Called when within scrollThreshold of the bottom
scrollThreshold number 50 Pixel distance from the bottom that triggers onScrolledBottom (use a higher value for earlier prefetching)

(event: Event) => void — the listener to attach. Closure deps are stable across renders (the consumer-supplied callbacks are passed once at construction time), so wrap the construction in a useEffect keyed on the same callbacks.

  • For virtualized infinite scroll, prefer DataTableVirtualizedBody’s onNearEnd + prefetchThreshold props over onScrolledBottomonNearEnd is virtualizer-index-driven (catches fast scrolls, scrollbar drag, programmatic scrollToIndex() jumps, and initial renders where the dataset doesn’t fill the viewport).
  • The handler reads event.currentTarget, so it must be attached directly to the scroll container — not to a parent.

Shared row-click guard used by every body component (DataTableBody, DataTableDndBody, DataTableDndColumnBody, DataTableVirtualizedBody, DataTableVirtualizedDndBody, DataTableVirtualizedDndColumnBody). Centralized so the interactive-element list (action buttons, checkboxes, links, radix collection items, etc.) stays in sync — adding a new data-slot to ignore means editing one file instead of six.

Returns true when the click landed on (or inside) an interactive element that should suppress the row-level onRowClick. Used by the per-row variants (DataTableBody, DataTableVirtualizedBody) which already know which row was clicked from event.currentTarget.dataset.rowId.

import { isInteractiveClickTarget } from "@/components/niko-table/lib/row-click"
const handleRowClick = (event: React.MouseEvent<HTMLElement>) => {
if (isInteractiveClickTarget(event.target as HTMLElement)) return
const rowId = event.currentTarget.dataset.rowId
if (!rowId) return
const row = table.getRow(rowId)
if (!row) return
onRowClick(row.original, event)
}

For delegated <tbody> handlers (used by the DnD bodies). Returns the matched row, or null if the click should be ignored — either it landed on an interactive descendant or no row could be resolved from the closest tr[data-row-id].

import { resolveRowFromClick } from "@/components/niko-table/lib/row-click"
const handleRowClick = (event: React.MouseEvent<HTMLElement>) => {
const row = resolveRowFromClick(event.target as HTMLElement, table)
if (!row) return
onRowClick(row.original, event)
}
  • The interactive-element list lives in isInteractiveClickTarget — extend it there to add new ignored elements.
  • Rows must carry data-row-id={row.id} for delegated resolution to work. All built-in bodies do this; custom bodies should follow suit.

System column IDs used for smart pinning and feature detection. These are the IDs for special columns like row selection and row expansion.

import { SYSTEM_COLUMN_IDS } from "@/components/niko-table/lib/constants"
// Available IDs:
SYSTEM_COLUMN_IDS.SELECT // "select" - Row selection checkbox column
SYSTEM_COLUMN_IDS.EXPAND // "expand" - Row expansion toggle column
SYSTEM_COLUMN_IDS.ACTIONS // "actions" - Row actions (kebab menu) column

Array version of SYSTEM_COLUMN_IDS values. Useful for filtering or checking if a column ID is a system column.

import { SYSTEM_COLUMN_ID_LIST } from "@/components/niko-table/lib/constants"
// ["select", "expand"]
const isSystemColumn = SYSTEM_COLUMN_ID_LIST.includes(columnId)

Standard error messages used throughout the library for consistent error reporting.

import { ERROR_MESSAGES } from "@/components/niko-table/lib/constants"
// Available messages:
ERROR_MESSAGES.DEPRECATED_GLOBAL_JOIN_OPERATOR

Used internally by faceted-filter components to translate a raw column dataset into the { label, value, count } shape the filter UI consumes. Documented for completeness — most consumers won’t import it directly.

Walks a TanStack Table column / row set and produces deduplicated { label, value, count } entries for the faceted-filter dropdown. Honors the dynamicCounts flag so counts update reactively when other filters change. Used internally by DataTableFacetedFilter and TableColumnFacetedFilter.

import { buildFacetedOptions } from "@/components/niko-table/lib/build-faceted-options"
const options = buildFacetedOptions(
table,
table.getCoreRowModel().rows,
"category", // accessorKey
table.getState().columnFilters,
table.getState().globalFilter,
{
limitToFilteredRows: true,
dynamicCounts: true,
showCounts: true,
autoOptionsFormat: "capitalize",
},
)
// → [{ label: "Electronics", value: "electronics", count: 12 }, ...]
  • This module is consumed by the faceted-filter wrappers; if you’re building a custom faceted UI, useGeneratedOptions (in hooks/) is the consumer-facing surface that wraps this helper with React state.
  • Runs in O(n) over the column’s cell values; safe to call per render for typical column counts.

Internal helpers that resolve globalFilter / column-filter combinations to a final visible row set. Used by DataTableRoot when wiring tableOptions.getFilteredRowModel.

Provides the row-filtering logic that backs the table’s filtered row model — combines per-column filters with the global filter, applies the right extendedFilter / globalFilter / numberRangeFilter / dateRangeFilter based on column meta, and returns the visible row set.

This module is internal to the table’s filter pipeline. Consumers normally interact with it indirectly via tableOptions.filterFns (which DataTableRoot wires automatically). If you’re building a fully custom filter pipeline you can import the named helpers from this file directly — see the source for the current export surface.

Helpers that keep a resizable table filling its container and its header labels legible. Flex fill and header-fit are on automatically once column resizing is enabled; the auto-fit and persistence helpers are opt-in.

Resolves which column(s) render widthless so they absorb the leftover row width (flex fill). Flex fill is on by default for resizable tables: the first non-pinned, resizable data column soaks up the surplus so the table fills its container instead of leaving dead space on the right. Pure layout — it never writes columnSizing, so there are no persistence side effects.

Overrides via column / table meta: meta.flex === true flexes that specific column, meta.flex === false opts a column out, and TableMeta.disableFlexFill === true turns fill off for the whole table. A column the user has explicitly resized is fixed at their width and hands the fill to the next eligible column.

import { resolveFlexColumnIds } from "@/components/niko-table/lib/flex-columns"
const flexIds = resolveFlexColumnIds(table)
// → Set { "name" } — the default first data column fills the surplus
Name Type Description
table Table<TData> TanStack Table instance

ReadonlySet<string> - Column ids to render widthless. Empty when fill is off (disableFlexFill, resizing disabled) or no eligible column exists.

The width to render for a single column. Centralizes the flex + header-fit rules so the header, body cells, and the min-width lock all agree. Returns the declared size when resizing is off, undefined for the flex column, exactly getSize() for a user-resized column, and otherwise floors the width at the header’s natural width so the label never truncates on load. Pure layout — headerMinWidths is measured, never written back into columnSizing.

import { resolveColumnWidth } from "@/components/niko-table/lib/flex-columns"
const width = resolveColumnWidth(column, {
resizing: true,
isFlex: flexIds.has(column.id),
columnSizing: table.getState().columnSizing,
headerMinWidths,
})
Name Type Description
column Column<TData> TanStack Table column instance
params.resizing boolean Whether column resizing is enabled
params.isFlex boolean Whether this column is a flex (fill) column
params.columnSizing ColumnSizingState Current column sizing state
params.headerMinWidths ReadonlyMap<string, number> Measured header widths (from useHeaderMinWidths)

number | string | undefined - The width to render (undefined for the flex column or an unsized off state).

Measures each column header’s natural width off-DOM (a reusable canvas, so no reflow) and floors un-resized columns at their header width — header-fit — so labels don’t truncate on load. Returns a columnId → minWidth map; a column the user has explicitly resized is left alone. Pure measurement, nothing is written to columnSizing. Recomputes only when the visible columns, their labels, or the header font change.

import { useHeaderMinWidths } from "@/components/niko-table/lib/use-header-min-widths"
const headerMinWidths = useHeaderMinWidths(table, scrollElement, resizingEnabled)
Name Type Description
table Table<TData> TanStack Table instance
scrollElement HTMLElement | null The table’s scroll container (for reading fonts)
enabled boolean Whether header-fit measurement is active

ReadonlyMap<string, number> - Minimum width in px per column id (label width + header chrome).

The even-split alternative to single-column flex fill. On load (and when the container grows or columns toggle) it grows the resizable columns by an equal share of the leftover space so they fill the available width, seeding columnSizing. Backs off permanently once the user resizes a column or a saved columnSizing is restored. Consumed by the opt-in <DataTableColumnAutoFit /> marker — most consumers drop that component rather than call this hook directly.

import { useColumnAutoFit } from "@/components/niko-table/lib/use-column-auto-fit"
useColumnAutoFit(table, scrollElement, resizingEnabled)
Name Type Description
table Table<TData> TanStack Table instance
scrollElement HTMLElement | null The table’s scroll container (measured for width)
enabled boolean Whether auto-fit is active (needs resizing on)

void - Side-effecting hook; seeds columnSizing via table.setColumnSizing.

Opt-in localStorage persistence for resized column widths. Column sizing is controlled state on <DataTableRoot>, so this hook owns it for you: wire the returned columnSizing + onColumnSizingChange into the table and widths survive reloads. Writes immediately (resizing runs in onEnd mode, so once per drag), syncs across tabs via the native storage event (an unchanged read keeps state identity, so idle tabs don’t re-render), and sanitizes corrupt / hand-edited storage so a bad value never reaches column.getSize().

import { useColumnSizingPersistence } from "@/components/niko-table/lib/use-column-sizing-persistence"
const { columnSizing, onColumnSizingChange, resetColumnSizing } =
useColumnSizingPersistence(`orders-table:${orgId}`)
<DataTableRoot
data={rows}
columns={columns}
state={{ columnSizing }}
onColumnSizingChange={onColumnSizingChange}
>
<DataTable>
<DataTableHeader />
<DataTableBody />
</DataTable>
<DataTableColumnResize />
</DataTableRoot>
Name Type Description
storageKey string localStorage key. Scope it per table (and per tenant/user).

ColumnSizingPersistence — an object with:

Property Type Description
columnSizing ColumnSizingState Controlled widths to pass to <DataTableRoot state={{ … }} />
onColumnSizingChange OnChangeFn<ColumnSizingState> Pass to <DataTableRoot onColumnSizingChange={…} />
resetColumnSizing () => void Clears stored widths and resets every column to its declared size