Skip to Content
UI PatternsTables

Tables

Coreola’s Table component is the workhorse of the admin UI. It handles server-side pagination, sort, filter, search, column visibility and reorder, CSV export, row selection, and URL-driven state — all configured by props, not by writing a custom table per page.

  • Source: src/components/table/Table.tsx
  • Types: src/components/table/table.types.ts
  • Hooks: src/hooks/useServerPaginatedTable.ts, src/hooks/useServerTableUrlState.ts, src/hooks/useExportAllRows.ts
  • Showcase: /core-components/data-display/table

When to use it

Use Table for any tabular dataset that:

  • Has more than ~20 potential rows (paginate at this point).
  • Has more than 3 columns.
  • Will be filtered, sorted, or searched.

For shorter, simpler lists, reach for DataList instead.


The prop surface

The most important props on Table<TRow>:

PropPurpose
idStable identifier used to persist preferences and namespace URL state
dataThe current row array (page slice in server mode)
columnsColumn definitions (see below)
filtersOptional filter config — see Filters
serverPaginationPagination metadata from the server
withCheckboxAdds the row-selection checkbox column
withSearchAdds the search input in the toolbar
withCustomColumnsAdds the column-visibility / reorder menu
withColumnsResizeEnables pointer-driven column resize
withExportAdds the CSV export button (gated by an ability/flag in callers)
withPaginationToggle the pagination footer (default true)
exportFilenameFilename for the CSV export
exportAllRowsCallback that fetches every row for export (server mode)
loadingShows skeleton rows + disables interactions
noDataTextEmpty-state label
getRowProps(row) => { onClick?, rowToggle? } — per-row interactivity
cardPropsSurrounding Card props (header, variant, etc.)
onTableStateChangeSnapshot callback for analytics or persistence

Column definitions

A column is a HeaderColumn:

{ id: 'status', label: 'Status', renderValue: ({ value, row }) => <Status value={value} />, exportValue: ({ value }) => String(value), optional: false, disableSort: false, disableResize: false, ellipsisOverflow: true, }

Key fields:

  • id — must match the row property the column reads.
  • label — header text (run through i18n in callers).
  • renderValue — receives { value, row, size }, returns a React node. Default renders the raw value.
  • exportValue — what goes into the CSV cell. Defaults to the rendered string.
  • optional — when true, the column starts hidden and can be enabled from the columns menu.
  • disableSort / disableResize — opt out of the corresponding feature for this column.
  • ellipsisOverflow — wrap long text in a TextOverflow truncation by default.

Server pagination

Server mode is the default for production data. The recipe:

  1. Call useServerPaginatedTable with the table id, filter config, default sort, and default page size.
  2. Pass the resulting serverPagination to <Table />.
  3. Use the returned queryParams in your RTK Query call.
const { isHydrated, tableState, totalRows, serverPagination, queryParams, } = useServerPaginatedTable({ tableId, filters }); const { data } = useGetAssessmentsQuery(queryParams, { skip: !isHydrated });

The hook waits for stored preferences from tableColumnsApi before issuing the first request — that is what isHydrated is for. Once hydrated, the URL is the source of truth and the row data follows.


URL-driven state

useServerTableUrlState (used internally by useServerPaginatedTable) namespaces every state param under the table id. For a table with id="assessments", the URL carries:

ParamHolds
assessments.page1-based page number
assessments.rowsPerPagePage size
assessments.searchFree-text search
assessments.sortColumnSorted column id
assessments.sortDirection'asc' or 'desc'
assessments.filter.<filterId>Filter values (comma-separated)
assessments.enabledFiltersActive optional filters

Effect: reloading the page or sharing the URL preserves the exact view. No additional code needed in your page.


Real usage

The assessment list, in src/features/collections/assessments/list/views/ListView.tsx:

<Table<AssessmentRow> id={tableId} data={assessments?.items} columns={columns} filters={filters} serverPagination={serverPagination} withSearch withCustomColumns withColumnsResize withExport={canExportTable} exportAllRows={exportAllRows} getRowProps={(row) => ({ onClick: () => navigate(`/details/${row.id}`) })} cardProps={{ variant: 'outlined', cardHeaderProps: { title: t('Assessments') } }} />

The columns and filters are built in a sibling hook (useAssessmentColumns, useAssessmentFilters) and passed in.


Row selection

Set withCheckbox and consume the selection via onTableStateChange:

<Table withCheckbox onTableStateChange={({ selected }) => setSelectedIds(selected)} ... />

The selected ids are also reflected in the URL under <tableId>.selected, so a refresh keeps the selection.


Row click and inline expansion

Use getRowProps to attach behavior per row:

getRowProps={(row) => ({ onClick: () => navigate(`/collections/customers/details/${row.id}`), })}

For expandable rows, return rowToggle with the JSX for the expansion content. The table handles open/close state internally.


CSV export

Two ways:

Current page only

Set withExport and let the table generate a CSV from data. Each column’s exportValue (or rendered text) becomes a cell.

All rows (server mode)

Provide exportAllRows — a callback that returns the full result set. The hook useExportAllRows builds this on top of the same RTK Query endpoint:

const exportAllRows = useExportAllRows({ fetch: triggerGetAllAssessments, queryParams, });

In both cases, the table writes a UTF-8 BOM and triggers a download with exportFilename (or a sensible default).


Persistence beyond the URL

The table also persists per-user preferences (column order, visibility, widths) to the backend via tableColumnsApi. This happens on onTableStateChange. On next visit, those preferences are merged with the URL state. Users get back what they configured, even from a different machine.


Loading and empty states

  • Loading: set loading={true} while data is fetching. The table renders skeleton rows matching column widths.
  • Empty: set noDataText to override the default “No data” message. Pair with a contextual EmptyState outside the table if the empty result is meaningful (e.g., “no assessments yet — create one”).

Anti-patterns

  • Rendering rows manually with a map over data, even “just this one time”. If the feature grows, you will rebuild filters/sort/paging/export from scratch.
  • Stuffing complex logic into renderValue. Memoize or extract a small component instead.
  • Skipping id because the table is “the only one on the page”. The id is required for URL state and persistence — set it.
  • Hard-coding column widths. Let the table size them; use disableResize only if a column must be a fixed shape.

Next steps

Last updated on