Grouping & Tree Data

Fold rows into expandable groups with aggregates, or nest them into a parent-child tree.

Row grouping organizes flat rows into an expandable folder-like structure based on identical column values. Tree data instead nests rows using an explicit parent-child relationship. Both are driven by initialState and both render through a custom groupRowRenderer.

Row grouping

Set groupBy to the fields to fold rows by, and aggDefs to compute summary metrics for each group:

import { useCallback, useMemo } from 'react';
import { Grid, type ColumnDef, type VisualRow, type GridApi } from '@witbybit/wit-grid-react';

interface EmployeeRow {
	id: string;
	name: string;
	department: string;
	salary: number;
}

export function GroupedEmployeesGrid({ data }: { data: EmployeeRow[] }) {
	const columns = useMemo<ColumnDef<EmployeeRow>[]>(
		() => [
			{ field: 'id', header: 'ID', width: 100 },
			{ field: 'name', header: 'Full Name', width: 180 },
			{ field: 'department', header: 'Department', width: 150 },
			{ field: 'salary', header: 'Salary', width: 120 },
		],
		[]
	);

	const groupRowRenderer = useCallback(({ visualRow, api }: { visualRow: VisualRow<EmployeeRow>; api: GridApi<EmployeeRow> }) => {
		if (visualRow.kind !== 'group') return null;
		return (
			<div onClick={() => api.toggleGroupExpanded(visualRow.id)} style={{ paddingLeft: visualRow.depth * 20 + 10 }}>
				{visualRow.expanded ? '▼' : '▶'} {visualRow.field}: {String(visualRow.key)} ({visualRow.childCount})
			</div>
		);
	}, []);

	return (
		<Grid
			rows={data}
			columns={columns}
			getRowId={(row) => row.id}
			initialState={{ groupBy: ['department'], groupRowHeight: 42 }}
			groupRowRenderer={groupRowRenderer}
		/>
	);
}

Group toggling

visualRow.kind === 'group' rows carry field, key, depth, childCount, and expanded. Toggle expansion with api.toggleGroupExpanded(visualRow.id), or drive it from outside the renderer with api.expandAllGroups() / api.collapseAllGroups().

Group footers can be enabled with initialState.showGroupFooter: true, and rendered with footerRowRenderer.

Tree data

For parent-child hierarchies (file trees, org charts, bill of materials), set getParentId instead of groupBy. Indentation isn't computed by the grid for you — walk the row's own parentId chain to derive its depth, and use that inside a cell renderer:

import { useMemo } from 'react';
import { Grid, type ColumnDef, type CellRendererProps } from '@witbybit/wit-grid-react';

interface FileNode {
	id: string;
	name: string;
	parentId?: string;
	size?: string;
}

function depthOf(nodeId: string, byId: Map<string, FileNode>): number {
	let depth = 0;
	let current = byId.get(nodeId);
	while (current?.parentId) {
		depth += 1;
		current = byId.get(current.parentId);
	}
	return depth;
}

function createTreeNameRenderer(byId: Map<string, FileNode>) {
	return function TreeNameRenderer({ value, row }: CellRendererProps<FileNode>) {
		const depth = depthOf(row.id, byId);
		const hasChildren = [...byId.values()].some((node) => node.parentId === row.id);
		return (
			<div style={{ paddingLeft: depth * 20 }}>
				{hasChildren ? '📁' : '📄'} {String(value)}
			</div>
		);
	};
}

export function FileDirectoryGrid({ nodes }: { nodes: FileNode[] }) {
	const byId = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]);
	const TreeNameRenderer = useMemo(() => createTreeNameRenderer(byId), [byId]);
	const columns = useMemo<ColumnDef<FileNode>[]>(
		() => [
			{ field: 'name', header: 'Node Path / Name', width: 300, renderer: { kind: 'react', component: TreeNameRenderer } },
			{ field: 'size', header: 'Capacity Size', width: 120 },
		],
		[TreeNameRenderer]
	);

	return (
		<Grid rows={nodes} columns={columns} getRowId={(row) => row.id} initialState={{ getParentId: (row) => row.parentId, groupRowHeight: 38 }} />
	);
}

groupBy and getParentId are mutually exclusive strategies for the same underlying grouped row model — use groupBy when groups come from repeated column values, getParentId when the hierarchy is already explicit in the data.

See Events for the groupByChanged, groupColumnAdded/Removed/Moved, and aggDefsChanged events emitted as grouping state changes.

Edit on GitHub

Last updated on

On this page