Skip to content

Row Features

Row Selection

useRowSelection manages checkbox-style row selection independently from the cell range selection handled by the interaction kernel.

tsx
import { useRowSelection, useClientRowModel, Grid } from "@hobom-grid/react";

type Employee = { id: number; name: string; salary: number };

function App({ data }: { data: Employee[] }) {
  const rowSel = useRowSelection<number>();
  const rowModel = useClientRowModel(data, { getId: (r) => r.id });

  const allIds = data.map((r) => r.id);

  return (
    <>
      <div>{rowSel.selectedCount} rows selected</div>

      <Grid
        rowCount={rowModel.rowCount + 1}
        colCount={3} // checkbox + name + salary
        renderCell={(cell) => {
          // Header row
          if (cell.rowIndex === 0) {
            if (cell.colIndex === 0) {
              return (
                <input
                  type="checkbox"
                  checked={rowSel.isAllSelected(allIds)}
                  onChange={() => rowSel.toggleAll(allIds)}
                />
              );
            }
            return <div>{["Name", "Salary"][cell.colIndex - 1]}</div>;
          }

          // Body rows
          const row = rowModel.getRow(cell.rowIndex - 1);
          if (cell.colIndex === 0) {
            return (
              <input
                type="checkbox"
                checked={rowSel.isSelected(row.id)}
                onChange={() => rowSel.toggleRow(row.id)}
              />
            );
          }
          return (
            <div style={{ background: rowSel.isSelected(row.id) ? "#e8f0fe" : undefined }}>
              {cell.colIndex === 1 ? row.name : `$${row.salary}`}
            </div>
          );
        }}
      />
    </>
  );
}

API

ts
const rowSel = useRowSelection<TId>();

TId can be any value usable as a Set member (string, number, object reference, etc.).

PropertyTypeDescription
selectedRowsReadonlySet<TId>The set of currently selected IDs
selectedCountnumberNumber of selected rows
isSelected(id)(TId) => booleanWhether the given ID is selected
toggleRow(id)(TId) => voidToggle selection of one row
selectAll(ids)(TId[]) => voidSelect exactly these rows (replaces current selection)
clearAll()() => voidClear all selections
toggleAll(ids)(TId[]) => voidSelect all if any are unselected; clear all if all are selected
isAllSelected(ids)(TId[]) => booleanReturns true only if every ID in the list is selected

Indeterminate Checkbox

For a proper "select all" checkbox with indeterminate state:

tsx
const isAll = rowSel.isAllSelected(allIds);
const isSome = rowSel.selectedCount > 0 && !isAll;

<input
  type="checkbox"
  ref={(el) => {
    if (el) el.indeterminate = isSome;
  }}
  checked={isAll}
  onChange={() => rowSel.toggleAll(allIds)}
/>;

useAggregateRow computes summary values (sum, average, count, min, max, or custom) over a dataset. Use it to render a sticky footer row in the grid.

tsx
import { useAggregateRow, Grid } from "@hobom-grid/react";

const FOOTER_ROW = -1; // Sentinel value to identify the footer row

type Employee = { name: string; salary: number; age: number };

function App({ data }: { data: Employee[] }) {
  const agg = useAggregateRow(data, [
    {
      key: "name",
      fn: "count",
      getValue: (r) => r.name,
      label: "Total",
    },
    {
      key: "salary",
      fn: "sum",
      getValue: (r) => r.salary,
      format: (v) => `$${(v as number).toLocaleString()}`,
    },
    {
      key: "age",
      fn: "avg",
      getValue: (r) => r.age,
      format: (v) => `Avg ${(v as number).toFixed(1)}`,
    },
  ]);

  const KEYS: (keyof Employee)[] = ["name", "salary", "age"];
  const TOTAL_ROWS = data.length + 2; // +1 header, +1 footer

  return (
    <Grid
      rowCount={TOTAL_ROWS}
      colCount={3}
      renderCell={(cell) => {
        // Header
        if (cell.rowIndex === 0) {
          return <div style={{ fontWeight: "bold" }}>{KEYS[cell.colIndex]}</div>;
        }
        // Footer (last row)
        if (cell.rowIndex === TOTAL_ROWS - 1) {
          return (
            <div style={{ fontWeight: "bold", background: "#f5f5f5" }}>
              {agg.getFormatted(KEYS[cell.colIndex])}
            </div>
          );
        }
        // Body
        const row = data[cell.rowIndex - 1];
        return <div>{String(row[KEYS[cell.colIndex]])}</div>;
      }}
    />
  );
}

Built-in Aggregation Functions

fnBehaviour
"sum"Numeric sum of all values
"avg"Numeric average; returns 0 if no rows
"count"Count of non-null, non-empty values
"min"Minimum numeric value
"max"Maximum numeric value

Custom Aggregation

Pass a function (values: unknown[]) => unknown for any custom logic:

tsx
{
  key: "activeCount",
  fn: (values) => values.filter(Boolean).length,
  getValue: (r) => r.active,
  format: (v) => `${v} active`,
}

AggColumnDef Options

OptionTypeRequiredDescription
keystringIdentifier used to retrieve the result
fnAggFn | functionAggregation function or built-in name
getValue(row: TRow) => unknownExtract the value from each row
format(v: unknown) => stringFormat the result for display
labelstringFallback label when result is null/zero

Result API

MethodTypeDescription
getValue(key)(string) => unknownRaw aggregated value
getFormatted(key)(string) => stringFormatted display string (applies format or falls back to label then String(value))

Performance

The aggregation only recalculates when rows or columns change identity. If columns is a constant array defined outside the component, you don't need to memoize it. If it's computed dynamically, wrap it in useMemo.