# React Integration

> Install and configure @toolbox-web/grid-react — feature props, JSX renderers, editors, hooks, and event handling.

The `@toolbox-web/grid-react` package provides React integration for the `<tbw-grid>` data grid component.

:::note[Where to find feature docs]
This page covers React-specific setup and APIs (feature props, hooks, providers). Core grid features and plugins — cell renderers, editors, events, master-detail, sorting, filtering, selection, etc. — are documented on the [Core](/grid/core.md) and [Plugins](/grid/plugins.md) pages, each with a React tab and runnable demos.
:::

## Compatibility

| React version | Support level |
| ------------- | ------------- |
| 19            | **Tested** — used in demos and CI |
| 18            | **Tested** |
| &lt; 18       | Not supported (minimum peer dependency) |

## Installation

#### npm

  ```bash
  npm install @toolbox-web/grid @toolbox-web/grid-react
  ```

#### yarn

  ```bash
  yarn add @toolbox-web/grid @toolbox-web/grid-react
  ```

#### pnpm

  ```bash
  pnpm add @toolbox-web/grid @toolbox-web/grid-react
  ```

#### bun

  ```bash
  bun add @toolbox-web/grid @toolbox-web/grid-react
  ```

## Setup

Register the grid web component in your application entry point:

```typescript
// main.tsx or index.tsx
import '@toolbox-web/grid';
```

## Basic Usage

The simplest way to use the grid is with the `DataGrid` component:

```tsx
import { useState } from 'react';
import { DataGrid } from '@toolbox-web/grid-react';

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

function EmployeeGrid() {
  const [employees] = useState<Employee[]>([
    { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
    { id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
    { id: 3, name: 'Charlie', department: 'Sales', salary: 85000 },
  ]);

  return (
    <DataGrid
      rows={employees}
      columns={[
        { field: 'id', header: 'ID', width: 70 },
        { field: 'name', header: 'Name', sortable: true },
        { field: 'department', header: 'Department', sortable: true },
        { field: 'salary', header: 'Salary', type: 'number' },
      ]}
      style={{ height: 400, display: 'block' }}
    />
  );
}
```

## Enabling Features with Props

Features are enabled using **declarative props** combined with **side-effect imports**. This gives you clean JSX and tree-shakeable bundles.

### How It Works

1. **Import the feature** - A side-effect import registers the feature factory
2. **Use the prop** - DataGrid detects the prop and creates the plugin instance

```tsx
// 1. Import features you need (once per feature)
import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/multi-sort';
import '@toolbox-web/grid-react/features/editing';
import '@toolbox-web/grid-react/features/filtering';

import { DataGrid } from '@toolbox-web/grid-react';

function EmployeeGrid() {
  return (
    <DataGrid
      rows={employees}
      columns={columns}
      // 2. Just use the props - plugins are created automatically!
      selection="range"           // SelectionPlugin with mode: 'range'
      multiSort                   // MultiSortPlugin
      editing="dblclick"          // EditingPlugin with editOn: 'dblclick'
      filtering                   // FilteringPlugin with defaults
    />
  );
}
```

### Why Side-Effect Imports?

- **Tree-shakeable** - Only the features you import are bundled
- **Synchronous** - No loading states, no HTTP requests, no spinners
- **Type-safe** - Full TypeScript support for feature props
- **Clean JSX** - No `plugins: [new SelectionPlugin({ mode: 'range' })]` boilerplate

### Available Features

| Import | Prop | Example |
|--------|------|---------|
| `features/selection` | `selection` | `selection="range"` or `selection={{ mode: 'row', checkbox: true }}` |
| `features/multi-sort` | `multiSort` | `multiSort` or `multiSort={{ maxSortColumns: 3 }}` |
| `features/filtering` | `filtering` | `filtering` or `filtering={{ debounceMs: 200 }}` |
| `features/editing` | `editing` | `editing="dblclick"` or `editing="click"` |
| `features/clipboard` | `clipboard` | `clipboard` (requires selection) |
| `features/undo-redo` | `undoRedo` | `undoRedo` (requires editing) |
| `features/context-menu` | `contextMenu` | `contextMenu` |
| `features/reorder-columns` | `reorderColumns` | `reorderColumns` (column drag-to-reorder) |
| `features/visibility` | `visibility` | `visibility` (column visibility panel) |
| `features/pinned-columns` | `pinnedColumns` | `pinnedColumns` |
| `features/grouping-columns` | `groupingColumns` | `groupingColumns` |
| `features/grouping-rows` | `groupingRows` | `groupingRows={{ groupOn: (row) => row.department }}` |
| `features/tree` | `tree` | `tree={{ childrenField: 'children' }}` |
| `features/master-detail` | `masterDetail` | `masterDetail` (use with `<GridDetailPanel>`) |
| `features/responsive` | `responsive` | `responsive` (card layout on mobile) |
| `features/export` | `export` | `export` |
| `features/print` | `print` | `print` |
| `features/pinned-rows` | `pinnedRows` | `pinnedRows` or `pinnedRows={{ position: 'bottom' }}` |
| `features/column-virtualization` | `columnVirtualization` | `columnVirtualization` |
| `features/reorder-rows` | `reorderRows` | `reorderRows` |
| `features/pivot` | `pivot` | `pivot={{ rowFields: ['category'], valueField: 'sales' }}` |
| `features/server-side` | `serverSide` | `serverSide={{ pageSize: 50 }}` |
| `features/sticky-rows` | `stickyRows` | `stickyRows={{ isSticky: 'isSection' }}` |
| `features/row-drag-drop` | `rowDragDrop` | `rowDragDrop` (cross-grid / external row drag-and-drop) |
| `features/tooltip` | `tooltip` | `tooltip` (cell / header tooltips) |

### Core Config Props (no import needed)

These props control core grid behaviour and don't require feature imports. See [`DataGridProps`](/grid/react/api/types/datagridprops.md) for the full prop surface and [`AllFeatureProps`](/grid/react/api/types/allfeatureprops.md) for every typed feature prop.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `rows` | `T[]` | `[]` | Row data — omit when using `serverSide` |
| `columns` | [`ColumnConfig`](/grid/api/core/interfaces/columnconfig.md)`[]` | — | Column definitions (alternative to `gridConfig.columns`) |
| `gridConfig` | [`GridConfig`](/grid/api/core/interfaces/gridconfig.md) | — | Full configuration object — always wrap in `useMemo` |
| `ref` | [`Ref<DataGridRef>`](/grid/react/api/types/datagridref.md) | — | Imperative handle (also returned by `useGrid()`) |
| `loading` | `boolean` | `false` | Show the grid's loading overlay |
| `fitMode` | `FitMode` | `'auto'` | Column-fit strategy |
| `customStyles` | `string` | — | CSS injected into the grid (renderer/editor styling) |
| `ssr` | `boolean` | `false` | **Deprecated** — no-op in practice; will be removed in a future major release |
| `sortable` | `boolean` | `true` | Grid-wide sorting toggle. Set `false` to disable all sorting. |
| `filterable` | `boolean` | `true` | Grid-wide filtering toggle. Requires FilteringPlugin. |
| `selectable` | `boolean` | `true` | Grid-wide selection toggle. Requires SelectionPlugin. |

```tsx
// Disable sorting and selection at runtime
<DataGrid
  sortable={false}
  selectable={false}
  selection="range"  // Plugin loaded but disabled via selectable={false}
/>
```

### Import All Features (Development)

For prototyping or when bundle size isn't critical:

```tsx
// Import all features at once
import '@toolbox-web/grid-react/features';

// Now all feature props work
<DataGrid selection="range" multiSort filtering editing="dblclick" clipboard />
```

## Custom Tool Panels

Use [`GridToolPanel`](/grid/react/api/components/gridtoolpanel.md) to declare a sidebar panel inline. The component renders a `<tbw-grid-tool-panel>` element in the grid's light DOM — the shell picks it up automatically, so **no `gridConfig.shell.toolPanels` registration is needed**. The `children` prop is a render function that receives a [`ToolPanelContext`](/grid/react/api/types/toolpanelcontext.md) (`{ grid }`).

```tsx
import { DataGrid, GridToolPanel, type ToolPanelContext } from '@toolbox-web/grid-react';

function EmployeeGrid() {
  return (
    <DataGrid rows={employees} columns={columns}>
      <GridToolPanel
        id="filters"
        title="Quick Filters"
        icon="🔍"
        order={10}
      >
        {({ grid }: ToolPanelContext) => (
          <div style={{ padding: 16 }}>
            <h3>Quick Filters</h3>
            <label>
              <input type="checkbox" /> Active Only
            </label>
          </div>
        )}
      </GridToolPanel>
    </DataGrid>
  );
}
```

**Props:** `id` (required), `title` (required), `icon`, `tooltip`, `order` (default `100`, lower = first).

## Header & Toolbar Content

Inject framework content into the grid's shell header (left zone) or toolbar (right zone) via [`GridHeaderContent`](/grid/react/api/components/gridheadercontent.md) and [`GridToolbarContent`](/grid/react/api/components/gridtoolbarcontent.md). These wrap the imperative `registerHeaderContent` / `registerToolbarContent` APIs so children render through React's normal reconciliation (state, context, hooks all work).

```tsx
import { DataGrid, GridHeaderContent, GridToolbarContent } from '@toolbox-web/grid-react';

function CalendarGrid() {
  const [year, setYear] = useState(new Date().getFullYear());
  return (
    <DataGrid rows={rows} columns={columns}>
      <GridHeaderContent id="calendar-nav" order={0}>
        <HeaderNav year={year} onYearChange={setYear} />
      </GridHeaderContent>
      <GridToolbarContent id="calendar-buttons" order={0}>
        <ToolbarNav onPrev={prev} onToday={today} onNext={next} />
      </GridToolbarContent>
    </DataGrid>
  );
}
```

**Props:** `id` (optional — auto-generated if omitted), `order` (default `100`, lower = first). Children re-render in place when state changes without re-registering with the grid.

## Using the `useGrid` Hook

The [`useGrid()`](/grid/react/api/hooks/usegrid.md) hook provides programmatic access to the grid — see [`UseGridReturn`](/grid/react/api/types/usegridreturn.md) for the full return shape. It exposes core grid operations only; **feature-specific operations (export, selection, filtering, etc.) live on the dedicated feature hooks** (`useGridSelection`, `useGridExport`, ...) listed below.

```tsx
import { DataGrid, useGrid } from '@toolbox-web/grid-react';

function EmployeeGrid() {
  const {
    ref,                  // Pass to DataGrid
    element,              // Direct grid element access
    isReady,              // True once the grid has booted
    config,               // Current effective GridConfig (snapshot)
    getConfig,            // async: re-read effective config
    forceLayout,          // async: force a layout pass
    toggleGroup,          // async: toggle a group row by key
    getPlugin,            // Get plugin instance by class
    getPluginByName,      // Get plugin instance by registered name
    registerStyles,       // Inject scoped CSS
    unregisterStyles,
    getVisibleColumns,    // Filtered list of non-hidden columns
  } = useGrid<Employee>();

  return (
    <div>
      <button onClick={() => forceLayout()}>Force Layout</button>
      <button disabled={!isReady} onClick={async () => console.log(await getConfig())}>
        Log Config
      </button>
      <DataGrid ref={ref} rows={employees} columns={columns} />
    </div>
  );
}
```

For selection, export, filtering, undo-redo, or print operations, use the feature hooks — they handle the per-plugin wiring:

```tsx
import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/export';
import { useGridSelection } from '@toolbox-web/grid-react/features/selection';
import { useGridExport } from '@toolbox-web/grid-react/features/export';

function Toolbar() {
  const { selectAll, clearSelection, getSelectedRows } = useGridSelection<Employee>();
  const { exportToCsv, isExporting } = useGridExport();

  return (
    <div className="toolbar">
      <button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}>
        Export CSV
      </button>
      <button onClick={selectAll}>Select All</button>
      <button onClick={() => console.log(getSelectedRows())}>Log Selection</button>
      <button onClick={clearSelection}>Clear</button>
    </div>
  );
}
```

### Multiple Grids

When a component contains multiple grids, pass a CSS selector to target a specific one:

```tsx
import { useGrid } from '@toolbox-web/grid-react';
import { useGridSelection } from '@toolbox-web/grid-react/features/selection';

function MultiGridPage() {
  // Target grids by selector instead of ref/context
  const primaryGrid = useGrid('#primary-grid');
  const selection = useGridSelection('#primary-grid');

  return (
    <div>
      <DataGrid id="primary-grid" rows={employees} selection="row" />
      <DataGrid id="secondary-grid" rows={departments} />
    </div>
  );
}
```

## Type-Level Defaults

Register application-wide renderers for data types using `GridTypeProvider` (see [`GridTypeProviderProps`](/grid/react/api/types/gridtypeproviderprops.md)) with a [`TypeDefaultsMap`](/grid/react/api/types/typedefaultsmap.md):

```tsx
import { GridTypeProvider, DataGrid, type TypeDefaultsMap } from '@toolbox-web/grid-react';

// Define type defaults for your application
const typeDefaults: TypeDefaultsMap = {
  currency: {
    format: (value: number) => '$' + value.toLocaleString(),
    renderer: ({ value }) => (
      <span style={{ color: value < 0 ? 'red' : 'green' }}>{value}</span>
    ),
  },
  date: {
    format: (value: string) => new Date(value).toLocaleDateString(),
  },
  boolean: {
    renderer: ({ value }) => <span>{value ? '✅' : '❌'}</span>,
  },
};

// Wrap your app (or a section) with the provider
function App() {
  return (
    <GridTypeProvider defaults={typeDefaults}>
      <EmployeeGrid />
    </GridTypeProvider>
  );
}

// Now columns with these types use the defaults automatically
function EmployeeGrid() {
  return (
    <DataGrid
      rows={employees}
      columns={[
        { field: 'salary', header: 'Salary', type: 'currency' },    // Uses currency format/renderer
        { field: 'hireDate', header: 'Hire Date', type: 'date' },   // Uses date format
        { field: 'isActive', header: 'Active', type: 'boolean' },   // Uses boolean renderer
      ]}
    />
  );
}
```

## Feature-Scoped Hooks

Feature imports export **scoped hooks** for type-safe programmatic access to plugin functionality:

```tsx
import '@toolbox-web/grid-react/features/export';
import { useGridExport } from '@toolbox-web/grid-react/features/export';
import { DataGrid } from '@toolbox-web/grid-react';

function EmployeeGrid() {
  const { exportToCsv, exportToExcel, isExporting } = useGridExport();

  return (
    <div>
      <button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}>
        Export CSV
      </button>
      <DataGrid rows={employees} columns={columns} export />
    </div>
  );
}
```

### Available Feature Hooks

See the **API Reference > Hooks** section for detailed method signatures, return types, and examples.

| Hook | Import | Key Methods |
|------|--------|-------------|
| [`useGridSelection()`](/grid/react/api/features/usegridselection.md) | `features/selection` | `selectAll`, `clearSelection`, `getSelection`, `getSelectedRows` |
| [`useGridFiltering()`](/grid/react/api/features/usegridfiltering.md) | `features/filtering` | `setFilter`, `clearAllFilters`, `getFilters`, `getFilteredRowCount` |
| [`useGridExport()`](/grid/react/api/features/usegridexport.md) | `features/export` | `exportToCsv`, `exportToExcel`, `exportToJson`, `isExporting` |
| [`useGridPrint()`](/grid/react/api/features/usegridprint.md) | `features/print` | `print`, `isPrinting` |
| [`useGridUndoRedo()`](/grid/react/api/features/usegridundoredo.md) | `features/undo-redo` | `undo`, `redo`, `canUndo`, `canRedo` |

## Overlay Editors (`useGridOverlay`)

Custom React editors that open a popover, listbox, calendar, or other floating panel typically render that panel into a portal so it can escape the cell's overflow clipping. Without help, the grid sees a click into the portal as a click _outside_ the editor and commits-and-exits the row before the user can pick an option.

The `useGridOverlay` hook bridges that gap by registering the panel with the grid as an external focus container — the React equivalent of Angular's `BaseOverlayEditor.initOverlay()`. Pair it with `aria-expanded` / `aria-controls` on the trigger so even editors that forget to call the hook get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see [issue #251](https://github.com/datafabric/toolbox-web/issues/251)).

```tsx
import { createPortal } from 'react-dom';
import { useId, useRef, useState } from 'react';
import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-react';

function AutocompleteEditor({ value, commit, cancel }: GridEditorContext<MyRow>) {
  const [open, setOpen] = useState(false);
  const panelRef = useRef<HTMLDivElement | null>(null);
  const listboxId = useId();

  // Registers panelRef.current with the grid while open=true; unregisters
  // automatically on close or unmount.
  useGridOverlay(panelRef, { open });

  return (
    <>
      <input
        role="combobox"
        aria-expanded={open}
        aria-controls={listboxId}
        defaultValue={String(value ?? '')}
        onClick={() => setOpen(true)}
        onKeyDown={(e) => {
          if (e.key === 'Escape') cancel();
        }}
      />
      {open &&
        createPortal(
          <div ref={panelRef} id={listboxId} role="listbox" className="my-listbox">
            {/* options that call commit(option) on click */}
          </div>,
          document.body,
        )}
    </>
  );
}
```

`useGridOverlay(panelRef, options?)` resolves the owning `<tbw-grid>` in this order:

1. `options.gridElement` if explicitly passed,
2. `panelRef.current.closest('tbw-grid')` for inline (non-portaled) panels,
3. The `GridElementContext` populated by `<DataGrid>` / `<GridProvider>`.

The grid host is also exposed on `ColumnEditorContext.grid` for editors that need to call grid APIs directly without using the hook.

## Manual Plugin Instantiation

Feature props are the recommended path. Use manual plugin instantiation only when you need a third-party plugin or a configuration the feature prop doesn't expose. Feature props and manual plugins can be mixed — the grid merges them.

```tsx
import { useMemo } from 'react';
import { DataGrid, type GridConfig } from '@toolbox-web/grid-react';
import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
import { MyCustomPlugin } from './my-custom-plugin';

function EmployeeGrid() {
  const config = useMemo<GridConfig<Employee>>(() => ({
    columns: [
      { field: 'id', header: 'ID' },
      { field: 'name', header: 'Name', editable: true },
    ],
    plugins: [
      new SelectionPlugin({ mode: 'range', checkbox: true }),
      new MyCustomPlugin({ /* third-party config */ }),
    ],
  }), []);

  return <DataGrid rows={employees} gridConfig={config} />;
}
```

> **Important**: Always use `useMemo` for config objects to prevent re-creating them on every render.

## Custom Icons

The grid supports two complementary ways to customize icons — see the [Theming Guide → Icon Customization](/grid/guides/theming.md#icon-customization) for both the CSS and JavaScript approaches:

- **CSS variables** (`--tbw-icon-*`) — preferred for themes and static customization; no JavaScript needed.
- **`gridConfig.icons`** — for dynamic icons, icon libraries, or `HTMLElement` instances; takes precedence over CSS.

`GridIconProvider` is the React wrapper for the JS path — it injects `icons` into `gridConfig.icons` for every descendant `<DataGrid>`:

```tsx
import { GridIconProvider, DataGrid } from '@toolbox-web/grid-react';

function App() {
  return (
    <GridIconProvider icons={{ sortAsc: '↑', sortDesc: '↓' }}>
      <DataGrid rows={employees} columns={columns} />
    </GridIconProvider>
  );
}
```

## Server-Side Data Loading

For backends that own paging / sorting / filtering, use the `serverSide` feature prop. Your `getRows` handler receives the current `sortModel`, `filterModel`, and block range — return rows and the total count. See [`ServerSideDataSource`](/grid/plugins/server-side/interfaces/serversidedatasource.md), [`GetRowsParams`](/grid/plugins/server-side/interfaces/getrowsparams.md), and [`GetRowsResult`](/grid/plugins/server-side/interfaces/getrowsresult.md) for the full contract — including the `params.signal` `AbortSignal` you can forward to `fetch()` for explicit cancellation.

```tsx
import '@toolbox-web/grid-react/features/server-side';
import '@toolbox-web/grid-react/features/multi-sort';
import { useMemo } from 'react';
import { DataGrid } from '@toolbox-web/grid-react';
import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side';

function ServerSideGrid() {
  const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({
    getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => {
      const params = new URLSearchParams({
        from: String(startNode),
        to: String(endNode),
      });
      if (sortModel?.length) {
        params.set(
          'sort',
          sortModel.map((s) => `${s.field}:${s.direction}`).join(','),
        );
      }
      if (filterModel) params.set('filter', JSON.stringify(filterModel));
      const response = await fetch(`/api/employees?${params}`, { signal });
      const { rows, totalNodeCount } = await response.json();
      return { rows, totalNodeCount };
    },
  }), []);

  return (
    <DataGrid
      columns={columns}
      multiSort
      serverSide={{ dataSource, pageSize: 100 }}
    />
  );
}
```

:::tip
Avoid `gridConfig.sortHandler` for server-side sort — it's bypassed when the multi-sort feature is loaded and only carries a single field. The `serverSide` feature is the supported path and works with both single- and multi-column sort.
:::

### React Query Integration

If your app already uses [TanStack Query](https://tanstack.com/query/latest), one
useful pairing is to back `ServerSidePlugin` with React Query's request cache.
Each block the grid asks for becomes a query keyed by the request shape
(`startNode`, `endNode`, `sortModel`, `filterModel`) — so scrolling back to a
previously fetched range is served from cache instead of re-hitting the API,
and you get retries, in-flight deduplication, and devtools for free.

The pattern: keep `getRows` thin and delegate fetching to `queryClient.fetchQuery`
with a stable key per block.

```tsx
import '@toolbox-web/grid-react/features/server-side';
import '@toolbox-web/grid-react/features/multi-sort';
import { useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { DataGrid } from '@toolbox-web/grid-react';
import type {
  GetRowsParams,
  GetRowsResult,
  ServerSideDataSource,
} from '@toolbox-web/grid/plugins/server-side';

async function fetchEmployeeBlock(
  params: GetRowsParams,
): Promise<GetRowsResult<Employee>> {
  const search = new URLSearchParams({
    from: String(params.startNode),
    to: String(params.endNode),
  });
  if (params.sortModel?.length) {
    search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(','));
  }
  if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel));
  const response = await fetch(`/api/employees?${search}`, { signal: params.signal });
  return response.json();
}

function ServerSideGrid() {
  const queryClient = useQueryClient();

  const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({
    // Each block becomes a cached query — React Query dedupes in-flight
    // requests, retries failed ones, and serves repeated scrolls from cache.
    getRows: (params) =>
      queryClient.fetchQuery({
        queryKey: [
          'employees',
          params.startNode,
          params.endNode,
          params.sortModel,
          params.filterModel,
        ],
        queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }),
        staleTime: 60_000,
      }),
  }), [queryClient]);

  return (
    <DataGrid
      columns={columns}
      multiSort
      serverSide={{ dataSource, pageSize: 100 }}
    />
  );
}
```

:::tip
When the user mutates a row (commit, paste, etc.), invalidate the query to
trigger a refresh: `queryClient.invalidateQueries({ queryKey: ['employees'] })`.
The grid will re-request the affected blocks on next scroll.
:::

## Dynamic Row Updates

```tsx
import { useState, useCallback } from 'react';
import { DataGrid } from '@toolbox-web/grid-react';

function EmployeeGrid() {
  const [employees, setEmployees] = useState<Employee[]>(initialData);

  const handleAdd = useCallback(() => {
    setEmployees(prev => [
      ...prev,
      { id: Date.now(), name: 'New Employee', department: '', salary: 50000 },
    ]);
  }, []);

  const handleDelete = useCallback(() => {
    // Remove first row as example
    setEmployees(prev => prev.slice(1));
  }, []);

  return (
    <div>
      <button onClick={handleAdd}>Add</button>
      <button onClick={handleDelete}>Remove First</button>
      <DataGrid rows={employees} columns={columns} />
    </div>
  );
}
```

## Performance Tips

### Memoize Config and Columns

```tsx
// ✅ Good: Memoized — stable reference
const config = useMemo<GridConfig<Employee>>(() => ({
  columns: [/* ... */],
}), []);

// ❌ Bad: Creates new config every render
const config: GridConfig<Employee> = { columns: [/* ... */] };
```

### Use `React.memo` for Cell Components

```tsx
const StatusBadge = React.memo(function StatusBadge({ status }: { status: string }) {
  return <span className={`badge badge--${status}`}>{status}</span>;
});
```

## Troubleshooting

### Feature prop not working

Ensure you imported the feature side-effect:

```tsx
// ❌ This won't work
<DataGrid selection="range" />

// ✅ Import the feature first
import '@toolbox-web/grid-react/features/selection';
<DataGrid selection="range" />
```

### Config object recreated on every render

Wrap in `useMemo`, or use feature props directly:

```tsx
// ✅ Best: Feature props — no config object needed
<DataGrid selection="row" />

// ✅ Also good: Stable config reference
const config = useMemo(() => ({ features: { selection: true } }), []);
```

### Grid not responding to row updates

Create new arrays — the grid uses reference equality:

```tsx
// ❌ Bad: Mutating existing array
employees.push(newEmployee);
setEmployees(employees);

// ✅ Good: New array reference
setEmployees([...employees, newEmployee]);
```

## See Also

- **API Reference** — Browse all components, hooks, and types via the React API section in the sidebar
- **[Grid Plugins](/grid/plugins.md)** — All available plugins
- **[Core Features](/grid/core.md)** — Variable row heights, events, column config
- **[Common Patterns](/grid/guides/common-patterns.md)** — Reusable recipes
