Skip to main content

useHeadlessGrid

The data layer every other hook sits on. Give it columns, data, and a stable getRowId; it returns the current page of sorted and filtered rows plus the state and actions to change them. It composes the same sort/filter/pagination sub-hooks <OGrid> uses internally, so behavior is identical to the component, including worker-thread sorting and server-side dataSource mode. It renders nothing.

Live demo​

Plain <table> driven by useHeadlessGrid: click headers to sort, filter, paginate, select rows
Live

Quick example​

import { useHeadlessGrid } from '@alaarab/ogrid-react';

const grid = useHeadlessGrid({
columns,
data: employees,
getRowId: (e) => e.id,
initialSort: { field: 'name', direction: 'asc' },
initialPageSize: 25,
});

<table>
<thead>
<tr>
{grid.columns.map((col) => (
<th key={col.columnId} onClick={() => grid.toggleSort(col.columnId)}>
{col.name} {grid.sortIndicator(col.columnId)}
</th>
))}
</tr>
</thead>
<tbody>
{grid.rows.map((row) => (
<tr key={grid.getRowId(row)}>
{grid.columns.map((col) => (
<td key={col.columnId}>{String(grid.getCellValue(row, col.columnId) ?? '')}</td>
))}
</tr>
))}
</tbody>
</table>

<input
value={grid.filters.name?.type === 'text' ? grid.filters.name.value : ''}
onChange={(e) => grid.setFilter('name', e.target.value ? { type: 'text', value: e.target.value } : undefined)}
/>
<button onClick={() => grid.setPage(grid.page + 1)} disabled={grid.page >= grid.totalPages}>Next</button>

Parameters​

UseHeadlessGridParams<T>

ParamTypeDefaultDescription
columnsIColumnDef<T>[]requiredColumn definitions. sortable, filterable, valueGetter, type, and friends are all honored.
dataT[]requiredFull client-side dataset. Pass [] when using dataSource.
getRowId(row: T) => RowIdrequiredStable row identity. Must return the same id for the same row across renders.
initialSort{ field: string; direction: 'asc' | 'desc' }noneInitial sort for uncontrolled mode.
initialFiltersIFilters{}Initial filters keyed by column id (or the column's filterField).
initialPagenumber1Initial page, 1-indexed.
initialPageSizenumber | 'all'25Initial page size. 'all' disables pagination.
sort / onSortChangeSortState / (sort) => voiduncontrolledControlled sort.
filters / onFiltersChangeIFilters / (filters) => voiduncontrolledControlled filters.
page / onPageChangenumber / (page) => voiduncontrolledControlled page.
pageSize / onPageSizeChangenumber | 'all' / (size) => voiduncontrolledControlled page size.
dataSourceIDataSource<T>noneServer-side mode. Sort, filter, and page are sent to the server and the returned items are shown as-is.
dataSourceKeystring | numbernoneIdentity of dataSource: refetch and reload filter options only when it changes, so dataSource can be an inline object. See Replacing the data source.
workerSortboolean | 'auto'offtrue always sorts in a Web Worker; 'auto' does so above roughly 5,000 rows.
onError(err: unknown) => voidnoneServer-side fetch error callback.
onFirstDataRendered() => voidnoneFired once when the first batch of rows lands.

Returns​

UseHeadlessGridResult<T>

FieldTypeDescription
columnsIColumnDef<T>[]The columns you passed in.
rowsT[]Rows on the current page after sort and filter. Render from this, and pass it to the spreadsheet hooks.
totalCountnumberPost-filter row count across all pages.
totalPagesnumberPage count at the current page size (1 when page size is 'all').
allFilteredRowsT[]Filtered and sorted rows across all pages. Client-side only; equals rows in server-side mode.
sortSortStateCurrent sort. field is '' when unsorted.
setSort(sort: SortState) => voidReplace the sort. Clear with { field: '', direction: 'asc' }.
toggleSort(columnId: string) => voidHeader-click behavior: a new column sorts ascending, the same column flips direction.
sortIndicator(columnId: string) => '▲' | '▼' | ''Ready-made indicator for header rendering.
filtersIFiltersCurrent filter map.
setFilters(filters: IFilters) => voidReplace all filters.
setFilter(key: string, value: FilterValue | undefined) => voidSet one filter; undefined clears it.
hasActiveFiltersbooleanAny filter set.
page, pageSizenumber, number | 'all'Current pagination state.
setPage, setPageSize(n) => voidPagination actions.
getRowId(row: T) => RowIdThe extractor you passed in.
getCellValue(row: T, columnId: string) => unknownResolve a cell value with full column semantics (valueGetter, nested keys). Returns undefined for unknown columns.
selectedRowIdsSet<RowId>Minimal row selection state.
isRowSelected(row: T) => boolean
toggleRowSelection(row: T) => void
selectAllOnPage() => voidAdds every row on the current page to the selection.
clearSelection() => void

Behavior notes​

  • Sorting or filtering resets to page 1, both for uncontrolled state and through onPageChange when you control it.
  • The page snaps back when data shrinks. If an uncontrolled page ends up past the last page (rows deleted, host-side filtering), the hook moves to the last page. Controlled page is left alone.
  • FilterValue is a discriminated union: { type: 'text', value }, { type: 'multiSelect', value: string[] }, { type: 'people', value }, or { type: 'date', value }. The filter key is the column id, or the column's filterField if set.
  • SortState is { field: string; direction: 'asc' | 'desc' }, exported from @alaarab/ogrid-react and both kits (since 2.17.4; older releases need UseHeadlessGridResult<T>['sort']).
  • Row selection is a plain Set that persists across pages and data changes. It is intentionally minimal; cell-range selection lives in useRangeSelection.
  • workerSort: 'auto' falls back to synchronous sorting when a column has a custom compare, when people filters are active, or when the Worker API is unavailable.
  • Server-side mode never loads the full set, so allFilteredRows is just the current page and totalCount comes from the server's totalCount.
  • Excel-like sort snapshots. Editing a cell after sorting does not re-sort the grid; the order is recomputed only when the sort changes.

See also​