When a client's requirements include inline editing, server-side pagination over 500,000 rows, Excel export, row grouping, and column pinning — all in the same table — the realistic shortlist narrows to one: AG Grid. I've used it across multiple ERP and data-intensive projects. It's genuinely excellent, but the documentation density and the gap between Community and Enterprise features can catch you off-guard.
Community vs Enterprise: Draw the Line Early
AG Grid Community is MIT-licensed and covers most use cases: sorting, filtering, pagination, cell editing, custom renderers. AG Grid Enterprise (commercial licence) adds row grouping, pivoting, Master-Detail rows, Excel export, range selection, clipboard, and the server-side row model with full lazy-loading support. If your project needs even one Enterprise feature, budget for the licence early — retrofitting it later means re-testing extensively.
- Community: client-side sorting/filtering, pagination, cell editing, custom cell/header renderers, themes.
- Enterprise only: Row Grouping, Pivot, Aggregation, Master-Detail, Excel Export, Server-Side Row Model, Range Selection, Clipboard, Sidebar panels.
The Server-Side Row Model for Large Datasets
For tables with more than a few thousand rows, the default Client-Side Row Model loads everything into the browser and filters/sorts in memory. This works well up to ~100k rows on fast machines, but becomes sluggish and memory-heavy beyond that. If you're pulling data from a database, the Server-Side Row Model (Enterprise) is the right choice: AG Grid asks your server for exactly the rows in the current viewport, and sends sort/filter params via the `IServerSideDatasource` interface.
const datasource: IServerSideDatasource = {
getRows: async (params: IServerSideGetRowsParams) => {
const { startRow, endRow, sortModel, filterModel } = params.request;
const { rows, totalCount } = await api.getInventory({
offset: startRow,
limit: endRow - startRow,
sort: sortModel,
filter: filterModel,
});
params.success({ rowData: rows, rowCount: totalCount });
},
};
<AgGridReact
rowModelType="serverSide"
serverSideDatasource={datasource}
cacheBlockSize={100}
/>Custom Cell Renderers
AG Grid renders cells using its own virtual DOM by default. You can replace this with any React component using `cellRenderer`. The component receives an `ICellRendererParams` object with the row data, value, and grid API.
import type { ICellRendererParams } from "ag-grid-react";
interface StatusCellProps extends ICellRendererParams {
value: "active" | "inactive" | "pending";
}
function StatusCell({ value }: StatusCellProps) {
const colour = { active: "green", inactive: "red", pending: "amber" }[value];
return (
<span className={`badge badge-${colour}`}>
{value}
</span>
);
}
// In column definition:
const colDefs: ColDef[] = [
{ field: "status", cellRenderer: StatusCell },
];Warning
React cell renderers introduce React reconciliation overhead per cell. For tables with thousands of visible cells, this can hurt scroll performance. Consider using plain HTML string renderers (`cellRenderer: (p) => `<span>...``) for simple cases.
Stable Row Identity with getRowId
Always define `getRowId` so AG Grid can track rows across updates. Without it, applying a data transaction (add, update, remove) re-renders the entire grid rather than patching the affected rows. This is the single most common source of AG Grid performance regressions I've debugged.
<AgGridReact
getRowId={(params) => params.data.id}
// Now applyTransaction({ update: [changedRow] }) only re-renders that one row
/>Theming in v32+
AG Grid v32 (released mid-2024) introduced a new CSS-variable-based theming system that replaced the old SCSS theming pipeline. You define a theme object with `themeQuartz`, `themeBalham`, or your own base, and customise via CSS variables on the grid container or globally. This finally makes AG Grid play nicely with Tailwind-based design systems.
import { themeQuartz } from "ag-grid-community";
const myTheme = themeQuartz.withParams({
accentColor: "var(--color-primary)",
backgroundColor: "var(--color-card)",
foregroundColor: "var(--color-card-foreground)",
borderColor: "var(--color-border)",
});
<AgGridReact theme={myTheme} />Tips That Saved Me Hours
- Always pass `columnDefs` through `useMemo` — recreating them on every render causes the grid to reset column state.
- Use `suppressColumnVirtualisation={false}` (the default) so off-screen columns aren't rendered. Only turn it on if you need to take screenshots of the full grid.
- Store and restore column state (widths, order, visibility, sorts, filters) via `gridApi.getColumnState()` / `gridApi.applyColumnState()` to persist user preferences.
- `onGridReady` is the correct place to capture the Grid API reference — not a ref on the component.
- When combining AG Grid with React Query, call `gridApi.setGridOption('datasource', ...)` inside `useEffect` rather than passing the datasource directly as a prop to avoid stale closure issues.