# DataSource Props

> Props Reference page for your DataSource in Infinite Table - with complete examples

Canonical page: https://infinite-table.com/docs/reference/datasource-props/

In the API Reference below we'll use **`DATA_TYPE`** to refer to the TypeScript type that represents the data the component is bound to.

### primaryKey (`string | (data: DATA_TYPE) => string`)

> The name of the id/primary key property of an item in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array. The value of this property needs to be unique.

This is probably one of the most important properties of the `<DataSource />` component, as it is used to identify items in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array.

Unlike with other DataGrid components, with `InfiniteTable` you don't need to have a column mapped to the primary key field.

The primary key is used internally by the component and is not displayed in the grid if you don't explicitly have a column bound to that field.

If the primary key is not unique, Infinite Table DataGrid won't work properly.

**Example: Simple demo of using primaryKey**

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

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

const employees: Employee[] = [
  {
    id: 1,
    name: 'Bob',
    salary: 10_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 2,
    name: 'Alice',
    salary: 20_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 3,
    name: 'John',
    salary: 30_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 4,
    name: 'Jane',
    salary: 35_000,
    department: 'Marketing',
    company: 'Janies',
  },
  {
    id: 5,
    name: 'Mary',
    salary: 40_000,
    department: 'Marketing',
    company: 'Janies',
  },
];

// this can be an array, a promise or a function returning array/promise
const data = new Promise<Employee[]>((resolve) => {
  setTimeout(() => {
    resolve(employees);
  }, 100);
});

export default function App() {
  return (
    <DataSource<Employee> data={data} primaryKey="id">
      <InfiniteTable<Employee>
        debugId="data-example"
        columnDefaultWidth={130}
        columns={columns}
      />
    </DataSource>
  );
}

const columns: InfiniteTablePropColumns<Employee> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 80,
  },
  name: {
    field: 'name',
  },
  salary: { field: 'salary', type: 'number' },
  department: { field: 'department', header: 'Dep.' },
  company: { field: 'company' },
};
```

The primary key can be either a string (the name of a property in the data object), or a function that returns a string.

Using functions (for more dynamic primary keys) is supported, but hasn't been tested extensively - so please report any issues you might encounter.

### treeExpandState (`TreeExpandStateValue`)

> Specifies the expand/collapse state of the tree nodes. See [`TreeExpandStateValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeExpandStateValue) for the shape of this object. For the uncontrolled version, see [`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState).

If no `treeExpandState` prop is specified, the tree will be rendered as fully expanded by default.

When using the controlled version, make sure to update the `treeExpandState` prop by using the [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) callback.

**Example: Using controlled tree expand state**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeExpandStateValue,
  TreeGrid,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  const [treeExpandState, setTreeExpandState] = useState<TreeExpandStateValue>({
    defaultExpanded: true,
    collapsedPaths: [['1', '10'], ['3']],
  });

  return (
    <>
      <TreeDataSource
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        treeExpandState={treeExpandState}
        onTreeExpandStateChange={setTreeExpandState}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              setTreeExpandState({ defaultExpanded: true, collapsedPaths: [] });
            }}
          >
            Expand all
          </button>
          <button
            onClick={() => {
              setTreeExpandState({
                defaultExpanded: false,
                expandedPaths: [],
              });
            }}
          >
            Collapse all
          </button>
          <div>
            Current tree expand state:
            <pre>{JSON.stringify(treeExpandState, null, 2)}</pre>
          </div>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### onTreeExpandStateChange (`(treeExpandState: TreeExpandStateValue, {dataSourceApi,nodePath, nodeState}) => void`)

> Called when the tree expand state changes.

When the user interacts with the tree (by expanding or collapsing a node), this callback is called with the new tree state.

The first parameter is the new tree state, and the second parameter is an object with the following properties:

- `dataSourceApi` - the [DataSource API](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceApi) instance
- `nodePath` - the path of the node that changed state. If the state was produced by an [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) call, this will be `null`.
- `nodeState` - the new state of the node (`"collapsed"` or `"expanded"`)

**Example: Using the onTreeExpandStateChange callback**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeExpandStateValue,
  TreeGrid,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  const [treeExpandState, setTreeExpandState] = useState<TreeExpandStateValue>({
    defaultExpanded: true,
    collapsedPaths: [['1', '10'], ['3']],
  });

  return (
    <>
      <TreeDataSource
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        treeExpandState={treeExpandState}
        onTreeExpandStateChange={setTreeExpandState}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              setTreeExpandState({ defaultExpanded: true, collapsedPaths: [] });
            }}
          >
            Expand all
          </button>
          <button
            onClick={() => {
              setTreeExpandState({
                defaultExpanded: false,
                expandedPaths: [],
              });
            }}
          >
            Collapse all
          </button>
          <div>
            Current tree expand state:
            <pre>{JSON.stringify(treeExpandState, null, 2)}</pre>
          </div>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### onNodeExpand (`(nodePath: NodePath, {dataSourceApi}) => void`)

> Called when a node is expanded. See related [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) and [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) props.

The [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) callbacks are called when a node is expanded or collapsed, respectively - either via user interaction or by an API call. However, they will not be called when the [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) methods are called.

### isNodeExpanded (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode<DATA_TYPE>, treeExpandState: TreeExpandState) => boolean`)

> Decides if the current (non-leaf) node is expanded. 

The inverse prop, [`isNodeCollapsed`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeCollapsed) is also available. Only one of these props can be specified.

If this prop is specified, [`treeSelectionState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelectionState) is ignored.

### isNodeCollapsed (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode<DATA_TYPE>, treeExpandState: TreeExpandState) => boolean`)

> Decides if the current (non-leaf) node is collapsed. See related [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) prop.

The inverse prop, [`isNodeExpanded`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeExpanded) is also available. Only one of these props can be specified.

If this prop is specified, [`treeSelectionState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelectionState) is ignored.

### isNodeReadOnly (`(rowInfo: InfiniteTable_Tree_RowInfoParentNode<DATA_TYPE>) => boolean`)

> Decides if the current (non-leaf) node can be expanded or collapsed and if the tree icon is disabled.

By default, parent nodes with `children: []` are read-only, meaning they won't respond to expand/collapse clicks.

However, if you specify a custom `isNodeReadOnly` function, you can change this behavior.

When a node is read-only, the [`expandNode`](https://infinite-table.com/docs/reference/tree-api/index.md#expandNode) and [`collapseNode`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseNode) methods need the `options.force` flag to be set to `true` in order to override the read-only restriction.

However, [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) and [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) will work regardless of the `isNodeReadOnly` setting.

For full control over the expand/collapse state of read-only nodes, you can use the [`isNodeExpanded`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeExpanded)/[`isNodeCollapsed`](https://infinite-table.com/docs/reference/datasource-props/index.md#isNodeCollapsed) props.

**Example: Using a custom isNodeReadOnly function**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeExpandStateValue,
  TreeGrid,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

const defaultTreeExpandState: TreeExpandStateValue = {
  defaultExpanded: true,
  collapsedPaths: [['1', '10']],
};

const returnFalse = () => false;
export default function App() {
  const [allowEmptyNodesExpand, setAllowEmptyNodesExpand] = useState(false);

  return (
    <>
      <TreeDataSource
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        defaultTreeExpandState={defaultTreeExpandState}
        isNodeReadOnly={allowEmptyNodesExpand ? returnFalse : undefined}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <pre>
            Empty parent nodes {allowEmptyNodesExpand ? 'can' : 'cannot'} be
            expanded.
          </pre>
          <button
            onClick={() => {
              setAllowEmptyNodesExpand(!allowEmptyNodesExpand);
            }}
          >
            Toggle read-only for empty nodes
          </button>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop - empty',
      sizeInKB: 1000,
      type: 'folder',
      children: [],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music - empty',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### onNodeCollapse (`(nodePath: NodePath, {dataSourceApi}) => void`)

> Called when a node is collapsed. See related [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange) props.

The [`onNodeExpand`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeExpand) and [`onNodeCollapse`](https://infinite-table.com/docs/reference/datasource-props/index.md#onNodeCollapse) callbacks are called when a node is expanded or collapsed, respectively - either via user interaction or by an API call. However, they will not be called when the [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) or [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll) methods are called.

### nodesKey (`string`)

> The name of the property in the data object that contains the child nodes for each tree node.

Only available when you're using the `<TreeDataSource />` component.

If not specified, it defaults to `"children"`.

Each node gets a `nodePath` property, which is the array with the ids of all the parent nodes leading down to the current node. The node path includes the id of the current node

```tsx {2} title="Node path vs row id"
const data = [
  { id: '1', name: 'Documents',                // path: ['1']
    children: [
      { id: '10', name: 'Private',             // path: ['1', '10']
        children: [
          { id: '100', name: 'Report.docx' },  // path: ['1', '10', '100'] 
          { id: '101', name: 'Vacation.docx' },// path: ['1', '10', '101']
        ],
      },
    ]
  },
  {
    id: '2',
    name: 'Downloads',                        // path: ['2']
    children: [
      {
        id: '20',
        name: 'cat.jpg',                      // path: ['2', '20']
      },
    ],
  },
];
```

**Example: Using a custom nodesKey prop**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
} from '@infinite-table/infinite-react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  nodes?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  return (
    <>
      <TreeDataSource nodesKey="nodes" primaryKey="id" data={dataSource}>
        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      nodes: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          nodes: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      nodes: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      nodes: [
        {
          id: '30',
          name: 'Music',
          sizeInKB: 0,
          type: 'folder',
          nodes: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          nodes: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### defaultTreeExpandState (`TreeExpandStateValue`)

> Specifies the expand/collapse state of the tree nodes. See [`TreeExpandStateValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeExpandStateValue) for the shape of this object. For the controlled version, see [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState).

**Example: Using uncontrolled tree expand state**

```ts
import {
  DataSourceApi,
  InfiniteTableColumn,
  TreeDataSource,
  TreeExpandStateValue,
  TreeGrid,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

const defaultTreeExpandState: TreeExpandStateValue = {
  defaultExpanded: true,
  collapsedPaths: [['1', '10'], ['3']],
};

export default function App() {
  const [dataSourceApi, setDataSourceApi] =
    useState<DataSourceApi<FileSystemNode> | null>();

  return (
    <>
      <TreeDataSource
        onReady={setDataSourceApi}
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        defaultTreeExpandState={defaultTreeExpandState}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              dataSourceApi!.treeApi.expandAll();
            }}
          >
            Expand all
          </button>
          <button
            onClick={() => {
              dataSourceApi!.treeApi.collapseAll();
            }}
          >
            Collapse all
          </button>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### isRowDisabled (`(rowInfo: InfiniteTableRowInfo<T>) => boolean`)

> This function ultimately decides the disabled state of a row. It overrides both [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState)/[`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) props.

It's called with a single argument - the [row info object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) for the row in question.

It should return `true` if the row is disabled, and `false` otherwise.

When this prop is used, [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange) will not be called.

### defaultRowDisabledState (`{enabledRows,disabledRows}`)

> The uncontrolled prop for managing row enabled/disabled state. For the controlled version see [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState). For listening to row disabled state changes, see [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange).

The value for this prop is an object with two properties:

- `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled.
- `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled.

The values in the `enabledRows`/`disabledRows` arrays are row ids, and not indexes.

This prop can be overriden by using the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-props/index.md#isRowDisabled) prop.

Here's an example of how to use the `defaultRowDisabledState` prop:

**Example: Using uncontrolled row disabled state**

Rows with ids `1`, `3`, `4` and `5` are disabled.

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

export default () => {
  return (
    <>
      <DataSource<Developer>
        data={data}
        primaryKey="id"
        defaultRowDisabledState={{
          enabledRows: true,
          disabledRows: [1, 3, 4, 5],
        }}
      >
        <InfiniteTable<Developer>
          debugId="defaultRowDisabledState-example"
          keyboardNavigation="row"
          columnDefaultWidth={120}
          columnMinWidth={50}
          columns={columns}
        />
      </DataSource>
    </>
  );
};
```

### rowDisabledState (`{enabledRows,disabledRows}`)

> Manages row enabled/disabled state. For the uncontrolled version see [`defaultRowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultRowDisabledState). For listening to row disabled state changes, see [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange).

The value for this prop is an object with two properties:

- `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled.
- `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled.

When using this controlled prop, you will need to update the `rowDisabledState` prop by using the [`onRowDisabledStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onRowDisabledStateChange) callback.

This prop can be overriden by using the [`isRowDisabled`](https://infinite-table.com/docs/reference/datasource-props/index.md#isRowDisabled) prop.

**Example: Using controlled row disabled state**

Rows with ids `1`, `3`, `4` and `5` are disabled initially.

Right click rows and use the context menu to enable/disable rows.

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  RowDisabledStateObject,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

export default () => {
  const [rowDisabledState, setRowDisabledState] = React.useState<
    RowDisabledStateObject<number>
  >({
    enabledRows: true,
    disabledRows: [1, 3, 4, 5],
  });
  return (
    <>
      <DataSource<Developer>
        data={data}
        primaryKey="id"
        rowDisabledState={rowDisabledState}
        onRowDisabledStateChange={(rowState) => {
          setRowDisabledState(rowState.getState());
        }}
      >
        <InfiniteTable<Developer>
          debugId="rowDisabledState-example"
          getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => {
            const rowDisabled = dataSourceApi.isRowDisabledAt(
              rowInfo.indexInAll,
            );
            return {
              columns: [{ name: 'label' }],
              items: [
                {
                  label: 'Disable row',
                  disabled: rowDisabled,
                  key: 'disable-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false);
                    hideMenu();
                  },
                },
                {
                  label: 'Enable row',
                  disabled: !rowDisabled,
                  key: 'enable-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabled(rowInfo.id, true);
                    hideMenu();
                  },
                },
                {
                  label: 'Toggle row enable/disable',
                  key: 'toggle-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabled(
                      rowInfo.id,
                      dataSourceApi.isRowDisabled(rowInfo.id),
                    );
                    hideMenu();
                  },
                },
              ],
            };
          }}
          keyboardNavigation="row"
          columnDefaultWidth={120}
          columnMinWidth={50}
          columns={columns}
        />
      </DataSource>
    </>
  );
};
```

### treeSelection (`TreeSelectionValue`)

> Determines what nodes are selected and deselected. For the uncontrolled version see [`defaultTreeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection).

The value of this prop determines if a node is selected or not.

See [`TreeSelectionValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue) for details on the shape of this object.

**Example: Using controlled tree selection**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
  TreeSelectionValue,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: {
    field: 'name',
    header: 'Name',
    renderTreeIcon: true,
    renderSelectionCheckBox: true,
  },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  const [treeSelection, setTreeSelection] = useState<TreeSelectionValue>({
    defaultSelection: false,
    selectedPaths: [['1', '10'], ['3']],
  });

  return (
    <>
      <TreeDataSource
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        treeSelection={treeSelection}
        onTreeSelectionChange={setTreeSelection}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              setTreeSelection({
                defaultSelection: false,
                selectedPaths: [],
              });
            }}
          >
            Deselect all
          </button>
          <button
            onClick={() => {
              setTreeSelection({
                defaultSelection: true,
                deselectedPaths: [],
              });
            }}
          >
            Select all
          </button>
          <div
            style={{
              overflow: 'auto',
              height: '10vh',
              minHeight: '200px',
            }}
          >
            Current tree selection:
            <pre>{JSON.stringify(treeSelection, null, 2)}</pre>
          </div>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### onTreeSelectionChange (`(treeSelection: TreeSelectionValue, context) => void`)

> Called when the tree selection changes. See [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection).

**Example: Reacting to tree selection changes**

```ts
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
  TreeSelectionValue,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: {
    field: 'name',
    header: 'Name',
    renderTreeIcon: true,
    renderSelectionCheckBox: true,
  },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  const [treeSelection, setTreeSelection] = useState<TreeSelectionValue>({
    defaultSelection: false,
    selectedPaths: [['1', '10'], ['3']],
  });

  return (
    <>
      <TreeDataSource
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        treeSelection={treeSelection}
        onTreeSelectionChange={setTreeSelection}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              setTreeSelection({
                defaultSelection: false,
                selectedPaths: [],
              });
            }}
          >
            Deselect all
          </button>
          <button
            onClick={() => {
              setTreeSelection({
                defaultSelection: true,
                deselectedPaths: [],
              });
            }}
          >
            Select all
          </button>
          <div
            style={{
              overflow: 'auto',
              height: '10vh',
              minHeight: '200px',
            }}
          >
            Current tree selection:
            <pre>{JSON.stringify(treeSelection, null, 2)}</pre>
          </div>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

When using `multi-row` [`selectionMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#selectionMode), the signature of this callback is:

 - `treeSelection` - the new [tree selection state](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue)
 - `context` - an object with the following properties:
   - `selectionMode` - will be `"multi-row"`
   - `lastUpdatedNodePath` - the path of the node that was last updated (either via user action or api call). Will be `null` of the action that triggered this callback was [`selectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#selectAll) or [`deselectAll`](https://infinite-table.com/docs/reference/tree-api/index.md#deselectAll).
   - `dataSourceApi` - the [DataSource API](https://infinite-table.com/docs/reference/datasource-api/index.md) instance

### defaultTreeSelection (`TreeSelectionValue`)

> Determines what nodes are selected and deselected. For the controlled version see [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection).

The value of this prop determines if a node is selected or not.

See [`TreeSelectionValue`](https://infinite-table.com/docs/reference/type-definitions/index.md#TreeSelectionValue) for details on the shape of this object.

**Example: Using uncontrolled tree selection**

```ts
import {
  DataSourceApi,
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
  TreeSelectionValue,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: {
    field: 'name',
    header: 'Name',
    renderTreeIcon: true,
    renderSelectionCheckBox: true,
  },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};
const defaultTreeSelection: TreeSelectionValue = {
  defaultSelection: false,
  selectedPaths: [['1', '10'], ['3']],
};
export default function App() {
  const [dataSourceApi, setDataSourceApi] =
    useState<DataSourceApi<FileSystemNode> | null>(null);
  return (
    <>
      <TreeDataSource
        onReady={setDataSourceApi}
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        defaultTreeSelection={defaultTreeSelection}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              dataSourceApi!.treeApi.deselectAll();
            }}
          >
            Deselect all
          </button>
          <button
            onClick={() => {
              dataSourceApi!.treeApi.selectAll();
            }}
          >
            Select all
          </button>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

### onRowDisabledStateChange (`(rowDisabledState) => void`)

> Called when the row disabled state changes.

It's called with just 1 argument (`rowDisabledState`), which is an instance of the `RowDisabledState` class. To get a literal object that represents the row disabled state, call the `rowDisabledState.getState()` method.

```tsx {3,19}
import {
  DataSource,
  RowDisabledStateObject,
} from '@infinite-table/infinite-react';
function App() {
  const [rowDisabledState, setRowDisabledState] = React.useState<
    RowDisabledStateObject<number>
  >({
    enabledRows: true,
    disabledRows: [1, 3, 4, 5],
  });
  return (
    <>
      <DataSource<Developer>
        data={data}
        primaryKey="id"
        rowDisabledState={rowDisabledState}
        onRowDisabledStateChange={(rowState) => {
          setRowDisabledState(rowState.getState());
        }}
      />
    </>
  );
}
```

When using the controlled [`rowDisabledState`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowDisabledState) prop, you will need to update the `rowDisabledState` by using this callback.

**Example: Using the onRowDisabledStateChange callback to update row disabled state**

Rows with ids `1`, `3`, `4` and `5` are disabled initially.

Right click rows and use the context menu to enable/disable rows.

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  RowDisabledStateObject,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

export default () => {
  const [rowDisabledState, setRowDisabledState] = React.useState<
    RowDisabledStateObject<number>
  >({
    enabledRows: true,
    disabledRows: [1, 3, 4, 5],
  });
  return (
    <>
      <DataSource<Developer>
        data={data}
        primaryKey="id"
        rowDisabledState={rowDisabledState}
        onRowDisabledStateChange={(rowState) => {
          setRowDisabledState(rowState.getState());
        }}
      >
        <InfiniteTable<Developer>
          debugId="rowDisabledState-example"
          getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => {
            const rowDisabled = dataSourceApi.isRowDisabledAt(
              rowInfo.indexInAll,
            );
            return {
              columns: [{ name: 'label' }],
              items: [
                {
                  label: 'Disable row',
                  disabled: rowDisabled,
                  key: 'disable-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false);
                    hideMenu();
                  },
                },
                {
                  label: 'Enable row',
                  disabled: !rowDisabled,
                  key: 'enable-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabled(rowInfo.id, true);
                    hideMenu();
                  },
                },
                {
                  label: 'Toggle row enable/disable',
                  key: 'toggle-row',
                  onAction: ({ hideMenu }) => {
                    dataSourceApi.setRowEnabled(
                      rowInfo.id,
                      dataSourceApi.isRowDisabled(rowInfo.id),
                    );
                    hideMenu();
                  },
                },
              ],
            };
          }}
          keyboardNavigation="row"
          columnDefaultWidth={120}
          columnMinWidth={50}
          columns={columns}
        />
      </DataSource>
    </>
  );
};
```

### aggregationReducers (`Record<string, DataSourceAggregationReducer>`)

> Specifies the functions to use for aggregating data. The object is a map where the keys are ids for aggregations and values are object of the shape described below.

The `DataSourceAggregationReducer` type can have the following properties

- `initialValue` - type `any`, mandatory for client-side aggregations. It can be a function, in which case, it will be called to compute the initial value for the aggregation. Otherwise, the initial value will be used as is.
- `field` - the field to aggregate on. Optional - if not specified, make sure you specify `getter`
- `getter`: `(data:T)=> any` - a getter function, called with the current `data` object.
- `reducer`: `string | (accumulator, value, data: T) => any` - either a string (for server-side aggregations) or a mandatory aggregation function for client-side aggregations.
- `done`: `(accumulator, arr: T[]) => any` - a function that is called to finish the aggregation after all values have been accumulated. The function should return the final value of the aggregation. Only used for client-side aggregations.
- `name` - useful especially in combination with [`pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy), as it will be used as the pivot column header.
- `pivotColumn` - if specified, will configure the pivot column generated for this aggregation. This object has the same shape as a normal [column](https://infinite-table.com/docs/reference/infinite-table-props.md#columns), but supports an extra `inheritFromColumn` property, which can either be a `string` (a column id), or a `boolean`. The default behavior for a pivot column is to inherit the configuration of the initial column that has the same `field` property. `inheritFromColumn` allows you to specify another column to inherit from, or, if `false` is passed, the pivot column will not inherit from any other column.

**Example: Aggregation demo - see `salary` column**

```ts files=["groupBy-example.page.tsx","columns.ts"]

```

Aggregation reducers can be used in combination with grouping and pivoting. The example below shows aggregations used with server-side pivoting

**Example: Aggregations used together with server-side pivoting**

```ts
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTablePropColumns,
  InfiniteTablePropColumnPinning,
  DataSourceData,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const DATA_SOURCE_SIZE = '10k';

const dataSource: DataSourceData<Developer> = ({
  pivotBy,
  aggregationReducers,
  groupBy,

  groupKeys = [],
}) => {
  const args = [
    pivotBy
      ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field })))
      : null,
    `groupKeys=${JSON.stringify(groupKeys)}`,
    groupBy
      ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field })))
      : null,
    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field: aggregationReducers[key].field,
            id: key,
            name: aggregationReducers[key].reducer,
          })),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');
  return fetch(
    process.env.NEXT_PUBLIC_BASE_URL +
      `/developers${DATA_SOURCE_SIZE}-sql?` +
      args,
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const aggregationReducers: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    // the aggregation name will be used as the column header
    name: 'Salary (avg)',
    field: 'salary',
    reducer: 'avg',
  },
  age: {
    name: 'Age (avg)',
    field: 'age',
    reducer: 'avg',
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

// make the row labels column (id: 'labels') be pinned
const defaultColumnPinning: InfiniteTablePropColumnPinning = {
  // make the generated group columns pinned to start
  'group-by-country': 'start',
  'group-by-stack': 'start',
};

// make all rows collapsed by default
const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function RemotePivotExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
        column: {
          // give the group column for the country prop a custom id
          id: 'group-by-country',
        },
      },
      {
        field: 'stack',
        column: {
          // give the group column for the stack prop a custom id
          id: 'group-by-stack',
        },
      },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'preferredLanguage' },
      {
        field: 'canDesign',
        // customize the column group
        columnGroup: ({ columnGroup }) => {
          return {
            ...columnGroup,
            header: `${
              columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer'
            }`,
          };
        },
        // customize columns generated under this column group
        column: ({ column }) => ({
          ...column,
          header: `🎉 ${column.header}`,
        }),
      },
    ],
    [],
  );

  return (
    <DataSource<Developer>
      primaryKey="id"
      data={dataSource}
      groupBy={groupBy}
      pivotBy={pivotBy}
      aggregationReducers={aggregationReducers}
      defaultGroupRowsState={groupRowsState}
      lazyLoad={true}
    >
      {({ pivotColumns, pivotColumnGroups }) => {
        return (
          <InfiniteTable<Developer>
            debugId="remote-pivoting-example"
            defaultColumnPinning={defaultColumnPinning}
            columns={columns}
            hideEmptyGroupColumns
            pivotColumns={pivotColumns}
            pivotColumnGroups={pivotColumnGroups}
            columnDefaultWidth={220}
          />
        );
      }}
    </DataSource>
  );
}
```

Pivot columns generated for aggregations will inehrit from initial columns - the example shows how to leverage this behavior and how to extend it

**Example: Pivot columns inherit from original columns bound to the same field**

```ts
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const columnAggregations: DataSourcePropAggregationReducers<Developer> = {
  avgSalary: {
    field: 'salary',
    name: 'Average salary',
    ...avgReducer,
  },
  avgAge: {
    field: 'age',
    ...avgReducer,
    pivotColumn: {
      defaultWidth: 500,
      inheritFromColumn: 'firstName',
    },
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: {
    field: 'firstName',
    style: {
      fontWeight: 'bold',
    },
    renderValue: ({ value }) => <>{value}!</>,
  },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  canDesign: { field: 'canDesign' },
  hobby: { field: 'hobby' },

  city: { field: 'city' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
    header: 'Salary',
    style: { color: 'red' },
  },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function PivotByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivot-column-inherit-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}
```

### data (`DATA_TYPE[]|Promise<DATA_TYPE[]|(params:DataSourceDataParams) => DATA_TYPE[]|Promise<DATA_TYPE[]>`)

> Specifies the data the component is bound to.

Can be one of the following:

- an array of the bound type - eg: `Employee[]`
- a Promise tha resolves to an array like the above
- a function that returns an any of the above

If the `data` prop is a function, it will be called with an object of type [`DataSourceDataParams`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceDataParams). [Click to see more details.](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourceDataParams)

**Example: Data loading example with promise**

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

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

const employees: Employee[] = [
  {
    id: 1,
    name: 'Bob',
    salary: 10_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 2,
    name: 'Alice',
    salary: 20_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 3,
    name: 'John',
    salary: 30_000,
    department: 'IT',
    company: 'Bobsons',
  },
  {
    id: 4,
    name: 'Jane',
    salary: 35_000,
    department: 'Marketing',
    company: 'Janies',
  },
  {
    id: 5,
    name: 'Mary',
    salary: 40_000,
    department: 'Marketing',
    company: 'Janies',
  },
];

// this can be an array, a promise or a function returning array/promise
const data = new Promise<Employee[]>((resolve) => {
  setTimeout(() => {
    resolve(employees);
  }, 100);
});

export default function App() {
  return (
    <DataSource<Employee> data={data} primaryKey="id">
      <InfiniteTable<Employee>
        debugId="data-example"
        columnDefaultWidth={130}
        columns={columns}
      />
    </DataSource>
  );
}

const columns: InfiniteTablePropColumns<Employee> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 80,
  },
  name: {
    field: 'name',
  },
  salary: { field: 'salary', type: 'number' },
  department: { field: 'department', header: 'Dep.' },
  company: { field: 'company' },
};
```

It's important to note you can re-fetch data by changing the reference you pass as the `data` prop to the `<DataSource/>` component. Passing another `data` function, will cause the component to re-execute the function and thus load new data.

**Example: Re-fetching data**

```ts files=["$DOCS/learn/working-with-data/refetch-example.page.tsx","$DOCS/learn/working-with-data/columns.ts"]

```

### defaultFilterValue (`{field?, id?, filter: {type, operator, value}[]`)

> Uncontrolled prop used for filtering. Can be used for both [client-side](https://infinite-table.com/docs/learn/filtering/filtering-client-side.md) and [server-side](https://infinite-table.com/docs/learn/filtering/filtering-server-side.md) filtering.

If you want to show the column filter editors, you have to either specify this property, or the controlled [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) - even if you have no initial filters. For no initial filters, use `defaultFilterValue=[]`.

For the controlled version, and more details on the shape of the objects in the array, see [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue).

**Example: Initial filtering applied via defaultFilterValue**

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  DataSourceProps,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

const domProps = {
  style: {
    height: '100%',
  },
};

const defaultFilterValue: DataSourceProps<Developer>['filterValue'] = [
  {
    field: 'salary',
    filter: {
      operator: 'gt',
      value: 50000,
      type: 'number',
    },
  },
];

export default () => {
  return (
    <>
      <p style={{ color: 'var(--infinite-cell-color)' }}>
        {`By default, only showing records with salary > 50000`}
      </p>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          defaultFilterValue={defaultFilterValue}
          filterDelay={0}
          filterMode="local"
        >
          <InfiniteTable<Developer>
            debugId="defaultFilterValue-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

You can control the visibility of the column filters by using the [`showColumnFilters`](https://infinite-table.com/docs/reference/infinite-table-props.md#showColumnFilters) prop.

### defaultRowSelection (`string|number|null|object`)

> Describes the selected row(s) in the `DataSource`

See more docs in the controlled version of this prop, [`rowSelection`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowSelection)

For single selection, the prop will be of type: `number | string | null`. Use `null` for empty selection in single selection mode.

For multiple selection, the prop will have the following shape:

```ts
const rowSelection = {
  selectedRows: [3, 6, 100, 23], // those specific rows are selected
  defaultSelection: false, // all rows deselected by default
};

// or
const rowSelection = {
  deselectedRows: [3, 6, 100, 23], // those specific rows are deselected
  defaultSelection: true, // all other rows are selected
};

// or, for grouped data - this example assumes groupBy=continent,country,city
const rowSelection = {
  selectedRows: [
    45, // row with id 45 is selected, no matter the group
    ['Europe', 'France'], // all rows in Europe/France are selected
    ['Asia'], // all rows in Asia are selected
  ],
  deselectedRows: [
    ['Europe', 'France', 'Paris'], // all rows in Paris are deselected
  ],
  defaultSelection: false, // all other rows are selected
};
```

For using group keys in the selection value, see related [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection)

**Example: Uncontrolled, multiple row selection with checkbox column**

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourcePropRowSelection,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
    renderSelectionCheckBox: true,
  },
  firstName: {
    field: 'firstName',
  },
  stack: {
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

const defaultRowSelection: DataSourcePropRowSelection = {
  selectedRows: [3, 5],
  defaultSelection: false,
};

export default function App() {
  return (
    <>
      <DataSource<Developer>
        data={dataSource}
        defaultRowSelection={defaultRowSelection}
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="uncontrolled-multiple-row-selection-example"
          columns={columns}
          columnDefaultWidth={150}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

### defaultSortInfo (`DataSourceSingleSortInfo<T>|DataSourceSingleSortInfo<T>[]|null`)

> Information for sorting the data. This is an uncontrolled prop.

For detailed explanations, see [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) (controlled property).

When you provide a `defaultSortInfo` prop and the sorting information uses a custom [sortType](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes), make sure you specify that as the `type` property of the sorting info object.

```tsx
defaultSortInfo={{
  field: 'color',
  dir: 1,
  // note this custom sort type
  type: 'color',
}}
```

You will need to have a property for that type in your [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) object as well.

```tsx
sortTypes={{
  color: (a, b) => //...
}}
```

**Example: Local uncontrolled single sorting**

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },

  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};
const shouldReloadData = {
  sortInfo: false,
};
export default function LocalUncontrolledSingleSortingExampleWithRemoteData() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        defaultSortInfo={{ field: 'salary', dir: -1 }}
        shouldReloadData={shouldReloadData}
      >
        <InfiniteTable<Developer>
          debugId="local-uncontrolled-single-sorting-example-with-remote-data"
          columns={columns}
          columnDefaultWidth={220}
        />
      </DataSource>
    </>
  );
}
```

**Example: Custom sort by color - magenta will come first**

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTableColumn,
} from '@infinite-table/infinite-react';
import * as React from 'react';

export type CarSale = {
  id: number;

  make: string;
  model: string;
  year: number;

  sales: number;
  color: string;
};

const carsales: CarSale[] = [
  {
    make: 'Volkswagen',
    model: 'GTI',
    year: 2009,
    sales: 6,
    color: 'red',
    id: 0,
  },
  {
    make: 'Honda',
    model: 'Element 2WD',
    year: 2009,
    sales: 739,
    color: 'red',
    id: 1,
  },
  {
    make: 'Acura',
    model: 'RDX 4WD',
    year: 2008,
    sales: 2,
    color: 'magenta',
    id: 2,
  },
  {
    make: 'Honda',
    model: 'Fit',
    year: 2009,
    sales: 211,
    color: 'blue',
    id: 3,
  },
  {
    make: 'Mazda',
    model: '6',
    year: 2009,
    sales: 31,
    color: 'blue',

    id: 4,
  },
  {
    make: 'Acura',
    model: 'TSX',
    year: 2009,
    sales: 14,
    color: 'yellow',
    id: 5,
  },
  {
    make: 'Acura',
    model: 'TSX',
    year: 2010,
    sales: 14,
    color: 'red',
    id: 6,
  },
  {
    make: 'Audi',
    model: 'A3',
    year: 2009,
    sales: 2,
    color: 'magenta',
    id: 7,
  },
];

const columns: Record<string, InfiniteTableColumn<CarSale>> = {
  color: { field: 'color', sortType: 'color' },
  make: { field: 'make' },
  model: { field: 'model' },

  sales: {
    field: 'sales',
    sortType: 'number',
  },
  year: {
    field: 'year',
    sortType: 'number',
  },
};
const newSortTypes = {
  color: (one: string, two: string) => {
    if (one === 'magenta') {
      // magenta comes first
      return -1;
    }
    if (two === 'magenta') {
      // magenta comes first
      return 1;
    }
    return one.localeCompare(two);
  },
};

export default function DataTestPage() {
  return (
    <DataSource<CarSale>
      data={carsales}
      primaryKey="id"
      defaultSortInfo={{
        field: 'color',
        dir: 1,
        type: 'color',
      }}
      sortTypes={newSortTypes}
    >
      <InfiniteTable<CarSale>
        debugId="customSortType-with-uncontrolled-sortInfo-example"
        columns={columns}
      />
    </DataSource>
  );
}
```

### filterDelay (`number`)

> The delay in milliseconds before the filter is applied. This is useful when you want to wait for the user to finish typing before applying the filter.

This is especially useful in order to reduce the number of requests sent to the server, when [remote filtering](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode) is used.

If not specified, defaults to `200` milliseconds. This means, any changes to the column filters, that happen inside a 200ms window (or the current value of [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay)), will be debounced and only the last value will be sent to the server.

If you want to prevent debouncing/batching filter values, you can set [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) to `0`.

### batchOperationDelay (`number`)

> The delay in milliseconds to wait before mutations are applied. This is useful to batch multiple mutations together.

If not specified, a `requestAnimationFrame` will be used to batch mutations.

The following mutative operations are batched:
 
 - [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData)
 - [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray)
 - [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData)
 - [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray)
 - [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData)
 - [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray)
 - [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData)
 - [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray)
 - [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey)
 - [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys)
 - [`replaceAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#replaceAllData)
 - [`clearAllData`](https://infinite-table.com/docs/reference/datasource-api/index.md#clearAllData)

### treeFilterFunction (`({ data, filterTreeNode, primaryKey }) => DATA_TYPE | boolean`)

> A function to be used for filtering a `TreeDataSource`.

The function should return a boolean value or a data object.

- when returning `false` the current data object will be filtered out.
- when returning `true`, the current data object will be included in the filtered data, with no changes.
- when returning a data object, the object will be used instead of the current data object for the row. This means that you can modify the data object to only include some of its children (which match a specific criteria)

The `treeFilterFunction` is called with an object that has a `filterTreeNode` function property. This function is a helper function you can use to continue the filtering further down the tree on the current (non-leaf) node.

This function will call the filtering function for each child of the current node. If all the children are filtered out, the current node will be filtered out as well. If there are any children that match the criteria, a clone of the current node will be returned with only the matching children.

You can opt to not use this helper function, and instead implement your own filtering logic. In this case, make sure you don't mutate data objects but rather return cloned versions of them.

**Example: Tree filtering via treeFilterFunction**

```ts file=tree-filter-function-example.page.tsx

```

### filterFunction (`({ data, dataArray, index, primaryKey }) => boolean`)

> A function to be used for client-side filtering.

Using this function will not show any special filtering UI for columns.

For filtering when using a `TreeGrid`, see [`treeFilterFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeFilterFunction).

**Example: Custom filterFunction example**

Loads data from remote location but will only show rows that have `id > 100`.

```ts
import * as React from 'react';

import {
  DataSourceData,
  DataSource,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
    filterType: 'salary',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency', defaultFilterable: false },
};

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          filterFunction={({ data }) => {
            return data.id > 100;
          }}
        >
          <InfiniteTable<Developer>
            debugId="custom-filter-function-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

### shouldReloadData.filterValue (`boolean`)

> Explicitly configures where filtering will take place and if changes in the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function.
 Replaces the deprecated [`filterMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterMode)

- `false` (the default) - filtering will be done on the client side and the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will not be invoked again.
- `true` - filtering will be done on the server side - the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will be called with an object that includes the `filterValue` property, so it can be sent to the server

### filterMode (`'local'|'remote'`)

> Explicitly configures where filtering will take place. Update to use the [`shouldReloadData.filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.filterValue) prop.

- `'local'` - filtering will be done on the client side
- `'remote'` - filtering will be done on the server side - the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function will be called with an object that includes the `filterValue` property, so it can be sent to the server

### filterTypes (`Record<string,{operators,emptyValues, defaultOperator}>`)

> Specifies the available types of filters for the columns.

A filter type is a concept that defines how a certain type of data is to be filtered.
A filter type will have a key, used to define the filter in the `filterTypes` object, and also the following properties:

- `label`
- `emptyValues` - an array of values considered to be empty values - when any of these values is used in the filter, the filter will match all records.
- `operators` - an array of operator this filter type supports
- `defaultOperator` - the default operator for the filter type
- `components` - an object that describes the custom components to be used for the filter type
  - `FilterEditor` - a custom filter editor component for this filter type
  - `FilterOperatorSwitch` - a custom component that is displayed at the left of the `FilterEditor` and can be used for switching between operators - only needed for very very advanced use-cases.

Let's imagine you have a `DataSource` with developers, each with a `salary` column, and for that column you want to allow `>`, `>=`, `<` and `<=` comparisons (operators).

For this, you would define the following filter type:

```tsx
const filterTypes = {
  income: {
    label: 'Income',
    emptyValues: ['', null, undefined],
    defaultOperator: 'gt',
    operators: [
      {
        name: 'gt',
        label: 'Greater than',
        fn: ({ currentValue, filterValue }) => {
          return currentValue > filterValue;
        },
      },
      {
        name: 'gte',
        //...
      },
      {
        name: 'lt',
        //...
      },
      {
        name: 'lte',
        //...
      },
    ],
  },
};
```

Each operator for a certain filter type needs to at least have a `name` and `fn` defined. The `fn` property is a function that will be called when client-side filtering is enabled, with an object that has the following properties:

- `currentValue` - the cell value of the current row for the column being filtered
- `filterValue` - the value of the filter editor
- `emptyValues` - the array of values considered to be empty values for the filter type
- `data` - the current row data object - `typeof DATA_TYPE`
- `index` - the index of the current row in the table - `number`
- `dataArray` - the array of all rows originally in the table - `typeof DATA_TYPE[]`
- `field?` - the field the current column is bound to (can be undefined if the column is not bound to a field)

**Example: Custom filter type used for the salary column**

The `salary` column has a custom filter type, with the following operators: `gt`, `gte`, `lt` and `lte`.

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
    filterType: 'salary',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency', defaultFilterable: false },
};

function getIcon(icon: string) {
  return () => (
    <div
      style={{
        width: 20,
        display: 'flex',
        justifyContent: 'center',
        flexFlow: 'row',
      }}
    >
      {icon}
    </div>
  );
}

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          defaultFilterValue={[]}
          filterDelay={0}
          filterMode="local"
          filterTypes={{
            salary: {
              defaultOperator: 'gt',
              emptyValues: ['', null, undefined],
              operators: [
                {
                  name: 'gt',
                  label: 'Greater Than',
                  components: {
                    Icon: getIcon('>'),
                  },
                  fn: ({ currentValue, filterValue }) => {
                    return currentValue > filterValue;
                  },
                },
                {
                  name: 'gte',
                  components: {
                    Icon: getIcon('>='),
                  },
                  label: 'Greater Than or Equal',
                  fn: ({ currentValue, filterValue }) => {
                    return currentValue >= filterValue;
                  },
                },
                {
                  name: 'lt',
                  components: {
                    Icon: getIcon('<'),
                  },
                  label: 'Less Than',
                  fn: ({ currentValue, filterValue }) => {
                    return currentValue < filterValue;
                  },
                },
                {
                  name: 'lte',
                  components: {
                    Icon: getIcon('<='),
                  },
                  label: 'Less Than or Equal',
                  fn: ({ currentValue, filterValue }) => {
                    return currentValue <= filterValue;
                  },
                },
              ],
            },
          }}
        >
          <InfiniteTable<Developer>
            debugId="filter-types-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

By default, the `string` and `number` filter types are available. You can import the default filter types like this:

```ts
import { defaultFilterTypes } from '@infinite-table/infinite-react';
```

If you want to make all your instances of `InfiniteTable` have new operators for those filter types, you can simply mutate the exported `defaultFilterTypes` object.

**Example: Enhanced string filter type - new 'Not includes' operator**

The `string` columns have a new `Not includes` operator.

```ts
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  defaultFilterTypes,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

defaultFilterTypes.string.operators.push({
  name: 'Not includes',
  label: 'Not Includes',
  fn: ({ currentValue, filterValue }) => {
    return (
      typeof currentValue === 'string' &&
      typeof filterValue == 'string' &&
      !currentValue.toLowerCase().includes(filterValue.toLowerCase())
    );
  },
});

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency', defaultFilterable: false },
};

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          defaultFilterValue={[]}
          filterDelay={0}
          filterMode="local"
        >
          <InfiniteTable<Developer>
            debugId="default-filter-types-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

When you specify new [`filterTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes), the default filter types of `string` and `number` are still available - unless the new object contains those keys and overrides them explicitly.

The current implementation of the default filter types is the following:

```tsx
export const defaultFilterTypes: Record<string, DataSourceFilterType<T>> = {
    string: {
      label: 'Text',
      emptyValues: [''],
      defaultOperator: 'includes',
      components: {
        FilterEditor: StringFilterEditor,
      },
      operators: [
        {
          name: 'includes',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'includes',
          fn: ({ currentValue, filterValue }) => {
            return (
              typeof currentValue === 'string' &&
              typeof filterValue == 'string' &&
              currentValue.toLowerCase().includes(filterValue.toLowerCase())
            );
          },
        },
        {
          label: 'Equals',
          components: {
            Icon: // custom icon as a React component ...
          },
          name: 'eq',
          fn: ({ currentValue: value, filterValue }) => {
            return typeof value === 'string' && value === filterValue;
          },
        },
        {
          name: 'startsWith',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'Starts With',
          fn: ({ currentValue: value, filterValue }) => {
            return value.startsWith(filterValue);
          },
        },
        {
          name: 'endsWith',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'Ends With',
          fn: ({ currentValue: value, filterValue }) => {
            return value.endsWith(filterValue);
          },
        },
      ],
    },
    number: {
      label: 'Number',
      emptyValues: ['', null, undefined],
      defaultOperator: 'eq',
      components: {
        FilterEditor: NumberFilterEditor,
      },
      operators: [
        {
          label: 'Equals',
          components: {
            Icon: // custom icon as a React component ...
          },
          name: 'eq',
          fn: ({ currentValue, filterValue }) => {
            return currentValue == filterValue;
          },
        },
        {
          label: 'Not Equals',
          components: {
            Icon: // custom icon as a React component ...
          },
          name: 'neq',
          fn: ({ currentValue, filterValue }) => {
            return currentValue != filterValue;
          },
        },
        {
          name: 'gt',
          label: 'Greater Than',
          components: {
            Icon: // custom icon as a React component ...
          },
          fn: ({ currentValue, filterValue, emptyValues }) => {
            if (emptyValues.includes(currentValue)) {
              return true;
            }
            return currentValue > filterValue;
          },
        },
        {
          name: 'gte',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'Greater Than or Equal',
          fn: ({ currentValue, filterValue, emptyValues }) => {
            if (emptyValues.includes(currentValue)) {
              return true;
            }
            return currentValue >= filterValue;
          },
        },
        {
          name: 'lt',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'Less Than',
          fn: ({ currentValue, filterValue, emptyValues }) => {
            if (emptyValues.includes(currentValue)) {
              return true;
            }
            return currentValue < filterValue;
          },
        },
        {
          name: 'lte',
          components: {
            Icon: // custom icon as a React component ...
          },
          label: 'Less Than or Equal',
          fn: ({ currentValue, filterValue, emptyValues }) => {
            if (emptyValues.includes(currentValue)) {
              return true;
            }
            return currentValue <= filterValue;
          },
        }
      ],
    },
  };
```

### filterTypes.components.FilterEditor

> A custom React component to be used as an editor for the current filter type

Every filter type can define the following `components`

- `FilterEditor` - a React component to be used as an editor for the current filter type
- `FilterOperatorSwitch` - a custom component that is displayed at the left of the `FilterEditor` and can be used for switching between operators - only needed for very very advanced use-cases.

Filter type operators can override the `FilterEditor` component - they can specify the following components:

- `FilterEditor` - if specified, it overrides the `FilterEditor` of the filter type
- `Icon` - a React component to be used as an icon for the operator - displayed by the menu triggered when clicking on the `FilterOperatorSwitch` component

**Example: Demo of a custom filter editor**

The `canDesign` column is using a custom `bool` filter type with a custom filter editor.

The checkbox has indeterminate state, which will match all values in the data source.

```ts
import * as React from 'react';

import {
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  components,
  useInfiniteColumnFilterEditor,
} from '@infinite-table/infinite-react';

const { CheckBox } = components;

type Developer = {
  id: number;
  firstName: string;
  canDesign: boolean;
  stack: string;
  hobby: string;
};
const dataSource: Developer[] = [
  {
    id: 1,
    firstName: 'John',

    canDesign: true,
    stack: 'frontend',
    hobby: 'gaming',
  },
  {
    id: 2,
    firstName: 'Jane',

    canDesign: false,
    stack: 'backend',
    hobby: 'reading',
  },
  {
    id: 3,
    firstName: 'Jack',

    canDesign: true,
    stack: 'frontend',
    hobby: 'gaming',
  },
  {
    id: 4,
    firstName: 'Jill',

    canDesign: false,
    stack: 'backend',
    hobby: 'reading',
  },
  {
    id: 5,
    firstName: 'Seb',

    canDesign: false,
    stack: 'backend',
    hobby: 'reading',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  canDesign: {
    field: 'canDesign',
    filterType: 'bool',
    renderValue: ({ value }) => (value ? 'Yes' : 'No'),
  },
  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
};

const domProps = {
  style: {
    height: '100%',
  },
};

function BoolFilterEditor() {
  const { value, setValue, className } =
    useInfiniteColumnFilterEditor<Developer>();

  return (
    <div className={className} style={{ textAlign: 'center' }}>
      <CheckBox
        checked={value}
        onChange={(newValue) => {
          if (value === true) {
            // after the value was true, make it go to indeterminate state
            newValue = null;
          }
          if (value === null) {
            // from indeterminate, goto false
            newValue = false;
          }
          setValue(newValue);
        }}
      />
    </div>
  );
}

export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={dataSource}
          primaryKey="id"
          defaultFilterValue={[]}
          filterDelay={0}
          filterTypes={{
            bool: {
              defaultOperator: 'eq',
              emptyValues: [null],
              components: {
                FilterEditor: BoolFilterEditor,
                FilterOperatorSwitch: () => null,
              },
              operators: [
                {
                  name: 'eq',
                  label: 'Equals',
                  fn: ({ currentValue, filterValue }) =>
                    currentValue === filterValue,
                },
              ],
            },
          }}
        >
          <InfiniteTable<Developer>
            debugId="custom-filter-editor-hooks-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

### filterValue (`{field?, id?, filter: {type, operator, value}[]`)

> Controlled prop used for filtering. Can be used for both [client-side](https://infinite-table.com/docs/learn/filtering/filtering-client-side.md) and [server-side](https://infinite-table.com/docs/learn/filtering/filtering-server-side.md) filtering.

For the uncontrolled version, see [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue)

If you want to show the column filter editors, you have to either specify this property, or the uncontrolled [`defaultFilterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultFilterValue) - even if you have no initial filters. For no initial filters, use `filterValue=[]`.

The objects in this array have the following shape:

- `filter` - an object describing the filter
  - `filter.value` - the value to filter by
  - `filter.type` - the current type of the filter (eg: `string`, `number` or another custom type you specify in the [filterTypes](https://infinite-table.com/docs/reference/datasource-props/index.md#filterTypes) prop)
  - `filter.operator` - the name of the operator being applied
- `field` - the field being filtered - generally matched with a column. This is optional, as some columns can have no field.
- `id` - the id of the column being filtered. This is optional - for columns bound to a field, the `field` should be used instead of the `id`.
- `disabled` - whether this filter is applied or not

**Example: Controlled filters with onFilterValueChange**

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  DataSourceProps,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  const [filterValue, setFilterValue] = React.useState<
    DataSourceProps<Developer>['filterValue']
  >([
    {
      field: 'salary',
      filter: {
        operator: 'gt',
        value: 50000,
        type: 'number',
      },
    },
  ]);

  return (
    <>
      <div style={{ color: 'var(--infinite-cell-color)' }}>
        Current filters:{' '}
        <pre>
          <code>{JSON.stringify(filterValue, null, 2)}</code>
        </pre>
      </div>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          filterValue={filterValue}
          onFilterValueChange={setFilterValue}
          filterDelay={0}
          filterMode="local"
        >
          <InfiniteTable<Developer>
            debugId="onFilterValueChange-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

You can control the visibility of the column filters by using the [`showColumnFilters`](https://infinite-table.com/docs/reference/infinite-table-props.md#showColumnFilters) prop.

### lazyLoad (`boolean|{batchSize:number}`)

> Whether the datasource will load data lazily - useful for server-side grouping and pivoting. If set to `true` or to an object (with `batchSize` property), the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) prop must be a function that returns a promise.

**Example: Server-side pivoting with full lazy load**

```ts
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTablePropColumns,
  InfiniteTablePropColumnPinning,
  DataSourceData,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const DATA_SOURCE_SIZE = '10k';

const dataSource: DataSourceData<Developer> = ({
  pivotBy,
  aggregationReducers,
  groupBy,

  groupKeys = [],
}) => {
  const args = [
    pivotBy
      ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field })))
      : null,
    `groupKeys=${JSON.stringify(groupKeys)}`,
    groupBy
      ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field })))
      : null,
    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field: aggregationReducers[key].field,
            id: key,
            name: aggregationReducers[key].reducer,
          })),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');
  return fetch(
    process.env.NEXT_PUBLIC_BASE_URL +
      `/developers${DATA_SOURCE_SIZE}-sql?` +
      args,
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const aggregationReducers: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    // the aggregation name will be used as the column header
    name: 'Salary (avg)',
    field: 'salary',
    reducer: 'avg',
  },
  age: {
    name: 'Age (avg)',
    field: 'age',
    reducer: 'avg',
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

// make the row labels column (id: 'labels') be pinned
const defaultColumnPinning: InfiniteTablePropColumnPinning = {
  // make the generated group columns pinned to start
  'group-by-country': 'start',
  'group-by-stack': 'start',
};

// make all rows collapsed by default
const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function RemotePivotExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
        column: {
          // give the group column for the country prop a custom id
          id: 'group-by-country',
        },
      },
      {
        field: 'stack',
        column: {
          // give the group column for the stack prop a custom id
          id: 'group-by-stack',
        },
      },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'preferredLanguage' },
      {
        field: 'canDesign',
        // customize the column group
        columnGroup: ({ columnGroup }) => {
          return {
            ...columnGroup,
            header: `${
              columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer'
            }`,
          };
        },
        // customize columns generated under this column group
        column: ({ column }) => ({
          ...column,
          header: `🎉 ${column.header}`,
        }),
      },
    ],
    [],
  );

  return (
    <DataSource<Developer>
      primaryKey="id"
      data={dataSource}
      groupBy={groupBy}
      pivotBy={pivotBy}
      aggregationReducers={aggregationReducers}
      defaultGroupRowsState={groupRowsState}
      lazyLoad={true}
    >
      {({ pivotColumns, pivotColumnGroups }) => {
        return (
          <InfiniteTable<Developer>
            debugId="remote-pivoting-example"
            defaultColumnPinning={defaultColumnPinning}
            columns={columns}
            hideEmptyGroupColumns
            pivotColumns={pivotColumns}
            pivotColumnGroups={pivotColumnGroups}
            columnDefaultWidth={220}
          />
        );
      }}
    </DataSource>
  );
}
```

### groupRowsState (`{collapsedRows:true|[][], expandedRows:true|[][]}`)

> Controls the expand/collapse state of group rows, when [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is used

See related [`defaultGroupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultGroupRowsState), [`onGroupRowsStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onGroupRowsStateChange) and [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy)

```tsx title="Specifying the state for group rows"
const groupRowsState: DataSourcePropGroupRowsStateObject = {
  collapsedRows: true,
  expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']],
};
```

The two properties in this object are `collapsedRows` and `expandedRows`, and each can have the following values:
 - `true` - meaning that all groups have this state
 - an array of arrays - representing the exceptions to the default value

So if you have `collapsedRows` set to `true` and then `expandedRows` set to `[['Mexico'], ['Mexico', 'backend'], ['India']]` then all rows are collapsed by default, except the rows specified in the `expandedRows`.

**Example: Using controlled expanded/collapsed state for group rows**

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  DataSourcePropGroupRowsStateObject,
  GroupRowsState,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'country',
  },
  {
    field: 'stack',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: { field: 'firstName' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },

  canDesign: { field: 'canDesign' },
  stack: { field: 'stack' },
};

export default function App() {
  const [groupRowsState, setGroupRowsState] = React.useState<
    DataSourcePropGroupRowsStateObject<string>
  >(() => {
    return {
      collapsedRows: true,
      expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']],
    };
  });

  const onGroupRowsStateChange = React.useCallback(
    (groupRowsState: GroupRowsState<string>) => {
      setGroupRowsState(groupRowsState.getState());
    },
    [],
  );

  return (
    <>
      <button
        onClick={() => {
          setGroupRowsState({
            expandedRows: true,
            collapsedRows: [],
          });
        }}
      >
        Expand all
      </button>
      <button
        onClick={() => {
          setGroupRowsState({
            expandedRows: [],
            collapsedRows: true,
          });
        }}
      >
        Collapse all
      </button>
      <DataSource<Developer>
        data={dataSource}
        primaryKey="id"
        groupBy={groupBy}
        groupRowsState={groupRowsState}
        onGroupRowsStateChange={onGroupRowsStateChange}
      >
        <InfiniteTable<Developer>
          debugId="group-rows-state-controlled-example"
          columns={columns}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

### onGroupRowsStateChange (`(state: GroupRowsState) => void`)

> Callback prop when the [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState) changes.

See related [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState) and [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy)

This function is called with an object that's an instance of [`GroupRowsState`](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState), when the user interacts with group rows and expands/collapses them.

If you want to get a plain object from this instance, call the `.getState()` method.

See [`GroupRowsState`](https://infinite-table.com/docs/reference/type-definitions/index.md#GroupRowsState) reference to find out all the utility methods this instance gives you.

### defaultGroupRowsState (`{collapsedRows:true|[][], expandedRows:true|[][]}`)

> Specifies the initial expand/collapse state of group rows, when [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) is used

For the controlled version, see related [`groupRowsState`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupRowsState).

```tsx title="Specifying the initial state for group rows"
const defaultGroupRowsState: DataSourcePropGroupRowsStateObject = {
  expandedRows: true,
  collapsedRows: [['Mexico'], ['Mexico', 'backend'], ['India']],
};
```

The two properties in this object are `collapsedRows` and `expandedRows`, and each can have the following values:
 - `true` - meaning that all groups have this state
 - an array of arrays - representing the exceptions to the default value

So if you have `expandedRows` set to `true` and then `collapsedRows` set to `[['Mexico'], ['Mexico', 'backend'], ['India']]` then all rows are expanded by default, except the rows specified in the `collapsedRows`.

**Example: Specifying initial expanded/collapsed state for group rows**
```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  DataSourcePropGroupRowsStateObject,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'country',
  },
  {
    field: 'stack',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: { field: 'firstName' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },

  canDesign: { field: 'canDesign' },
  stack: { field: 'stack' },
};
const defaultGroupRowsState: DataSourcePropGroupRowsStateObject<string> = {
  collapsedRows: true,
  expandedRows: [['Mexico'], ['Mexico', 'backend'], ['India']],
};

export default function App() {
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      groupBy={groupBy}
      defaultGroupRowsState={defaultGroupRowsState}
    >
      <InfiniteTable<Developer>
        debugId="group-rows-initial-state-example"
        columns={columns}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

### groupBy

> An array of objects with `field` properties, that control how rows are being grouped.

Each item in the array can have the following properties:

- field - `keyof DATA_TYPE`
- column - config object for the group [column](https://infinite-table.com/docs/reference/infinite-table-props.md#column) - see [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column).

When using [groupRenderStrategy="multi-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), it can be very useful for each group to configure it's own column - use [`groupBy.column`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy.column) for this.

See [`DataSourcePropGroupBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePropGroupBy) for the type definition.

**Example**

```ts files=["groupBy-example.page.tsx","columns.ts"]

```

### pivotBy (`DataSourcePivotBy<DATA_TYPE>[]`)

> An array of objects with `field` properties that control how pivoting works. Pivoting is very often associated with aggregations, so see related [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#aggregationReducers) for more details.

Each item in the array can have the following properties:

- field - `keyof DATA_TYPE`
- column - config object or function for generated pivot columns.

For more details on the type of the items in this array prop, see [`DataSourcePivotBy`](https://infinite-table.com/docs/reference/type-definitions/index.md#DataSourcePivotBy).

**Example: Pivoting with customized pivot column**

```ts
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  DataSourcePivotBy,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  field: 'salary',
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const columnAggregations: DataSourcePropAggregationReducers<Developer> = {
  salary: avgReducer,
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  canDesign: { field: 'canDesign' },
  hobby: { field: 'hobby' },

  city: { field: 'city' },
  age: { field: 'age' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

export default function PivotByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
        column: ({ column: pivotCol }) => {
          const lastKey =
            pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1];

          return {
            header: lastKey === 'yes' ? '💅 Designer' : '💻 Non-designer',
          };
        },
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivoting-customize-column-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}
```

 
### groupBy.column (`Partial<InfiniteTableColumn<T>>`)

> An object that configures how the column for the current group should look like

If [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) is specified, it overrides this property (the objects actually get merged, with `groupColumn` having higher priority and being merged last).

If you are using a [groupRenderStrategy="single-column"](https://infinite-table.com/docs/reference/infinite-table-props.md#groupRenderStrategy), then using `groupBy.column` should not be used, as you could have many groups with conflicting column configuration.

In this case, use the [`groupColumn`](https://infinite-table.com/docs/reference/infinite-table-props.md#groupColumn) prop.

**Example**

This example uses `groupBy.column` to configure the generated columns corresponding to each group.

```ts files=["groupBy-multi-with-column-example.page.tsx","columns.ts"]

```

### livePagination (`boolean`)

> Whether the component should use live pagination.

Use this in combination with [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) and [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange)

**Example: Live pagination - with react-query**

```ts
import '@infinite-table/infinite-react/index.css';
import {
  InfiniteTable,
  InfiniteTableColumn,
  DataSource,
  DataSourceSingleSortInfo,
  DataSourceDataParams,
  DataSourceLivePaginationCursorFn,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useCallback } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
  keepPreviousData,
} from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

const emptyArray: Employee[] = [];

export const columns: Record<string, InfiniteTableColumn<Employee>> = {
  id: { field: 'id' },
  country: {
    field: 'country',
  },
  city: { field: 'city' },
  team: { field: 'team' },
  department: { field: 'department' },
  firstName: { field: 'firstName' },
  lastName: { field: 'lastName' },

  salary: { field: 'salary' },
  age: { field: 'age' },
};

type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: number;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

const PAGE_SIZE = 10;

const dataSource = ({
  sortInfo,
  livePaginationCursor = 0,
}: {
  sortInfo: DataSourceSingleSortInfo<Employee> | null;
  livePaginationCursor: number;
}) => {
  return fetch(
    process.env.NEXT_PUBLIC_BASE_URL +
      `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${
        sortInfo?.dir === 1 ? 'asc' : 'desc'
      }&_start=${livePaginationCursor}`,
  )
    .then(async (r) => {
      const data = await r.json();
      // we need the remote count, so we take it from headers
      const total = Number(r.headers.get('X-Total-Count')!);
      return { data, total };
    })
    .then(({ data, total }: { data: Employee[]; total: number }) => {
      const page = livePaginationCursor / PAGE_SIZE + 1;

      const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0);
      return {
        data,
        hasMore: total > PAGE_SIZE * page,
        page,
        prevPageCursor,
        nextPageCursor: prevPageCursor + data.length,
      };
    })
    .then(
      (
        response,
      ): Promise<{
        data: Employee[];
        hasMore: boolean;
        page: number;
        nextPageCursor: number;
        prevPageCursor: number;
      }> => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(response);
          }, 150);
        });
      },
    );
};

const Example = () => {
  const [dataParams, setDataParams] = React.useState<
    Partial<DataSourceDataParams<Employee>>
  >({
    groupBy: [],
    sortInfo: undefined,
    livePaginationCursor: null,
  });
  const {
    data,
    fetchNextPage: fetchNext,
    isFetchingNextPage,
  } = useInfiniteQuery({
    initialPageParam: 0,
    queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy],
    queryFn: ({ pageParam = 0 }) => {
      const params = {
        livePaginationCursor: pageParam,
        sortInfo:
          dataParams.sortInfo as DataSourceSingleSortInfo<Employee> | null,
      };

      return dataSource(params);
    },

    placeholderData: keepPreviousData,
    getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0,
    getNextPageParam: (lastPage) => {
      const nextPageCursor = lastPage.hasMore
        ? lastPage.nextPageCursor
        : undefined;

      return nextPageCursor;
    },

    select: (data) => {
      const flatData = data.pages.flatMap((x) => x.data);
      const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor;

      const result = {
        pages: flatData,
        pageParams: [nextPageCursor],
      };

      return result;
    },
  });

  const onDataParamsChange = useCallback(
    (dataParams: DataSourceDataParams<Employee>) => {
      const params = {
        groupBy: dataParams.groupBy,
        sortInfo: dataParams.sortInfo,
        livePaginationCursor: dataParams.livePaginationCursor,
      };

      setDataParams(params);
    },
    [],
  );

  const [scrollTopId, setScrollTop] = React.useState(0);

  React.useEffect(() => {
    // when sorting changes, scroll to the top
    setScrollTop(Date.now());
  }, [dataParams.sortInfo]);

  const fetchNextPage = () => {
    if (isFetchingNextPage) {
      return;
    }

    fetchNext();
  };

  React.useEffect(() => {
    fetchNextPage();
  }, [dataParams.livePaginationCursor]);

  const livePaginationCursorFn: DataSourceLivePaginationCursorFn<Employee> =
    useCallback(({ length }) => {
      return length;
    }, []);

  return (
    <React.StrictMode>
      <DataSource<Employee>
        primaryKey="id"
        // take the data from `data.pages`,
        // as returned from our react-query select function

        data={data?.pages || emptyArray}
        loading={isFetchingNextPage}
        onDataParamsChange={onDataParamsChange}
        livePagination
        livePaginationCursor={livePaginationCursorFn}
      >
        <InfiniteTable<Employee>
          debugId="live-pagination-example"
          scrollTopKey={scrollTopId}
          columnDefaultWidth={200}
          columns={columns}
        />
      </DataSource>
    </React.StrictMode>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}

export default App;
```

### onDataMutations (`({ mutations, dataArray, primaryKeyField }) => void`)

> Callback prop to be called when the data changes via the DataSource API.

Called when any of the following methods have been called in the `DataSource` api

- [`updateData`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateData)
- [`updateDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#updateDataArray)

- [`removeData`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeData)
- [`removeDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArray)

- [`removeDataByPrimaryKey`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataByPrimaryKey)
- [`removeDataArrayByPrimaryKeys`](https://infinite-table.com/docs/reference/datasource-api/index.md#removeDataArrayByPrimaryKeys)

- [`insertData`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertData)
- [`insertDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#insertDataArray)

- [`addData`](https://infinite-table.com/docs/reference/datasource-api/index.md#addData)
- [`addDataArray`](https://infinite-table.com/docs/reference/datasource-api/index.md#addDataArray)

This callback is called with an object that has the following properties:

- `primaryKeyField` - the field configured as the primary key for the `<DataSource />`
- `mutations` - a `Map` with mutations. The keys in the map are primary keys of the mutated data

The values in the mutations are object descriptors of mutations, that have the following shape:

- `type`: `'insert'|'update'|'delete'`
- `originalData`: `DATA_TYPE | null` - the original data before the mutation. In case of `insert`, it will be `null`
- `data`: `Partial<DATA_TYPE>` - the updates to be performed on the data. In case of `delete`, it will be `undefined`. This is an object that will contain the primary key, and the updated values for the data (not necessarily the full object, except for `insert`, where it will be of type `DATA_TYPE`).

### livePaginationCursor (`string|number|((params) =>string|number)`)

> A cursor value for live pagination. A good value for this is the id of the last item in the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) array. It can also be defined as a function

Use this in combination with [`livePagination`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePagination) and [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange)

When this is a function, it is called with a parameter object that has the following properties:

- `array` - the current array of data
- `lastItem` - the last item in the array
- `length` - the length of the data array

**Example: Live pagination - with react-query**

```ts
import '@infinite-table/infinite-react/index.css';
import {
  InfiniteTable,
  InfiniteTableColumn,
  DataSource,
  DataSourceSingleSortInfo,
  DataSourceDataParams,
  DataSourceLivePaginationCursorFn,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useCallback } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
  keepPreviousData,
} from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

const emptyArray: Employee[] = [];

export const columns: Record<string, InfiniteTableColumn<Employee>> = {
  id: { field: 'id' },
  country: {
    field: 'country',
  },
  city: { field: 'city' },
  team: { field: 'team' },
  department: { field: 'department' },
  firstName: { field: 'firstName' },
  lastName: { field: 'lastName' },

  salary: { field: 'salary' },
  age: { field: 'age' },
};

type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: number;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

const PAGE_SIZE = 10;

const dataSource = ({
  sortInfo,
  livePaginationCursor = 0,
}: {
  sortInfo: DataSourceSingleSortInfo<Employee> | null;
  livePaginationCursor: number;
}) => {
  return fetch(
    process.env.NEXT_PUBLIC_BASE_URL +
      `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${
        sortInfo?.dir === 1 ? 'asc' : 'desc'
      }&_start=${livePaginationCursor}`,
  )
    .then(async (r) => {
      const data = await r.json();
      // we need the remote count, so we take it from headers
      const total = Number(r.headers.get('X-Total-Count')!);
      return { data, total };
    })
    .then(({ data, total }: { data: Employee[]; total: number }) => {
      const page = livePaginationCursor / PAGE_SIZE + 1;

      const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0);
      return {
        data,
        hasMore: total > PAGE_SIZE * page,
        page,
        prevPageCursor,
        nextPageCursor: prevPageCursor + data.length,
      };
    })
    .then(
      (
        response,
      ): Promise<{
        data: Employee[];
        hasMore: boolean;
        page: number;
        nextPageCursor: number;
        prevPageCursor: number;
      }> => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(response);
          }, 150);
        });
      },
    );
};

const Example = () => {
  const [dataParams, setDataParams] = React.useState<
    Partial<DataSourceDataParams<Employee>>
  >({
    groupBy: [],
    sortInfo: undefined,
    livePaginationCursor: null,
  });
  const {
    data,
    fetchNextPage: fetchNext,
    isFetchingNextPage,
  } = useInfiniteQuery({
    initialPageParam: 0,
    queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy],
    queryFn: ({ pageParam = 0 }) => {
      const params = {
        livePaginationCursor: pageParam,
        sortInfo:
          dataParams.sortInfo as DataSourceSingleSortInfo<Employee> | null,
      };

      return dataSource(params);
    },

    placeholderData: keepPreviousData,
    getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0,
    getNextPageParam: (lastPage) => {
      const nextPageCursor = lastPage.hasMore
        ? lastPage.nextPageCursor
        : undefined;

      return nextPageCursor;
    },

    select: (data) => {
      const flatData = data.pages.flatMap((x) => x.data);
      const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor;

      const result = {
        pages: flatData,
        pageParams: [nextPageCursor],
      };

      return result;
    },
  });

  const onDataParamsChange = useCallback(
    (dataParams: DataSourceDataParams<Employee>) => {
      const params = {
        groupBy: dataParams.groupBy,
        sortInfo: dataParams.sortInfo,
        livePaginationCursor: dataParams.livePaginationCursor,
      };

      setDataParams(params);
    },
    [],
  );

  const [scrollTopId, setScrollTop] = React.useState(0);

  React.useEffect(() => {
    // when sorting changes, scroll to the top
    setScrollTop(Date.now());
  }, [dataParams.sortInfo]);

  const fetchNextPage = () => {
    if (isFetchingNextPage) {
      return;
    }

    fetchNext();
  };

  React.useEffect(() => {
    fetchNextPage();
  }, [dataParams.livePaginationCursor]);

  const livePaginationCursorFn: DataSourceLivePaginationCursorFn<Employee> =
    useCallback(({ length }) => {
      return length;
    }, []);

  return (
    <React.StrictMode>
      <DataSource<Employee>
        primaryKey="id"
        // take the data from `data.pages`,
        // as returned from our react-query select function

        data={data?.pages || emptyArray}
        loading={isFetchingNextPage}
        onDataParamsChange={onDataParamsChange}
        livePagination
        livePaginationCursor={livePaginationCursorFn}
      >
        <InfiniteTable<Employee>
          debugId="live-pagination-example"
          scrollTopKey={scrollTopId}
          columnDefaultWidth={200}
          columns={columns}
        />
      </DataSource>
    </React.StrictMode>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}

export default App;
```

### onDataParamsChange (`(dataParams: DataSourceDataParams<DATA_TYPE:>)=>void`)

> A function to be called when data-related state changes.

Can be used to implement [`livePagination`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePagination)

The function is called with an object that has the following properties:

- `sortInfo` - current sort information - see [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) for details
- `groupBy` - current grouping information - see [`groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#groupBy) for details
- `filterValue` - current filtering information - see [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) for details
- `livePaginationCursor` - the value for the live pagination cursor - see [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) for details
- `changes` - an object that can help you figure out what change caused `onDataParamsChange` to be called.

**Example: Live pagination - with react-query**

```ts
import '@infinite-table/infinite-react/index.css';
import {
  InfiniteTable,
  InfiniteTableColumn,
  DataSource,
  DataSourceSingleSortInfo,
  DataSourceDataParams,
  DataSourceLivePaginationCursorFn,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useCallback } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
  keepPreviousData,
} from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

const emptyArray: Employee[] = [];

export const columns: Record<string, InfiniteTableColumn<Employee>> = {
  id: { field: 'id' },
  country: {
    field: 'country',
  },
  city: { field: 'city' },
  team: { field: 'team' },
  department: { field: 'department' },
  firstName: { field: 'firstName' },
  lastName: { field: 'lastName' },

  salary: { field: 'salary' },
  age: { field: 'age' },
};

type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: number;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

const PAGE_SIZE = 10;

const dataSource = ({
  sortInfo,
  livePaginationCursor = 0,
}: {
  sortInfo: DataSourceSingleSortInfo<Employee> | null;
  livePaginationCursor: number;
}) => {
  return fetch(
    process.env.NEXT_PUBLIC_BASE_URL +
      `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${
        sortInfo?.dir === 1 ? 'asc' : 'desc'
      }&_start=${livePaginationCursor}`,
  )
    .then(async (r) => {
      const data = await r.json();
      // we need the remote count, so we take it from headers
      const total = Number(r.headers.get('X-Total-Count')!);
      return { data, total };
    })
    .then(({ data, total }: { data: Employee[]; total: number }) => {
      const page = livePaginationCursor / PAGE_SIZE + 1;

      const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0);
      return {
        data,
        hasMore: total > PAGE_SIZE * page,
        page,
        prevPageCursor,
        nextPageCursor: prevPageCursor + data.length,
      };
    })
    .then(
      (
        response,
      ): Promise<{
        data: Employee[];
        hasMore: boolean;
        page: number;
        nextPageCursor: number;
        prevPageCursor: number;
      }> => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(response);
          }, 150);
        });
      },
    );
};

const Example = () => {
  const [dataParams, setDataParams] = React.useState<
    Partial<DataSourceDataParams<Employee>>
  >({
    groupBy: [],
    sortInfo: undefined,
    livePaginationCursor: null,
  });
  const {
    data,
    fetchNextPage: fetchNext,
    isFetchingNextPage,
  } = useInfiniteQuery({
    initialPageParam: 0,
    queryKey: ['employees', dataParams.sortInfo, dataParams.groupBy],
    queryFn: ({ pageParam = 0 }) => {
      const params = {
        livePaginationCursor: pageParam,
        sortInfo:
          dataParams.sortInfo as DataSourceSingleSortInfo<Employee> | null,
      };

      return dataSource(params);
    },

    placeholderData: keepPreviousData,
    getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0,
    getNextPageParam: (lastPage) => {
      const nextPageCursor = lastPage.hasMore
        ? lastPage.nextPageCursor
        : undefined;

      return nextPageCursor;
    },

    select: (data) => {
      const flatData = data.pages.flatMap((x) => x.data);
      const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor;

      const result = {
        pages: flatData,
        pageParams: [nextPageCursor],
      };

      return result;
    },
  });

  const onDataParamsChange = useCallback(
    (dataParams: DataSourceDataParams<Employee>) => {
      const params = {
        groupBy: dataParams.groupBy,
        sortInfo: dataParams.sortInfo,
        livePaginationCursor: dataParams.livePaginationCursor,
      };

      setDataParams(params);
    },
    [],
  );

  const [scrollTopId, setScrollTop] = React.useState(0);

  React.useEffect(() => {
    // when sorting changes, scroll to the top
    setScrollTop(Date.now());
  }, [dataParams.sortInfo]);

  const fetchNextPage = () => {
    if (isFetchingNextPage) {
      return;
    }

    fetchNext();
  };

  React.useEffect(() => {
    fetchNextPage();
  }, [dataParams.livePaginationCursor]);

  const livePaginationCursorFn: DataSourceLivePaginationCursorFn<Employee> =
    useCallback(({ length }) => {
      return length;
    }, []);

  return (
    <React.StrictMode>
      <DataSource<Employee>
        primaryKey="id"
        // take the data from `data.pages`,
        // as returned from our react-query select function

        data={data?.pages || emptyArray}
        loading={isFetchingNextPage}
        onDataParamsChange={onDataParamsChange}
        livePagination
        livePaginationCursor={livePaginationCursorFn}
      >
        <InfiniteTable<Employee>
          debugId="live-pagination-example"
          scrollTopKey={scrollTopId}
          columnDefaultWidth={200}
          columns={columns}
        />
      </DataSource>
    </React.StrictMode>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  );
}

export default App;
```

### onFilterValueChange (`({field?, id?, filter: {type, operator, value}[]) => void`)

> Callback prop called when the [`filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterValue) changes.

This might not be called immediately, as there might be a [`filterDelay`](https://infinite-table.com/docs/reference/datasource-props/index.md#filterDelay) set.

**Example: Controlled filters with onFilterValueChange**

```ts
import * as React from 'react';

import {
  DataSource,
  DataSourceData,
  DataSourceProps,
  InfiniteTable,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    defaultFilterable: true,
    field: 'salary',
    type: 'number',
  },

  firstName: {
    field: 'firstName',
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  const [filterValue, setFilterValue] = React.useState<
    DataSourceProps<Developer>['filterValue']
  >([
    {
      field: 'salary',
      filter: {
        operator: 'gt',
        value: 50000,
        type: 'number',
      },
    },
  ]);

  return (
    <>
      <div style={{ color: 'var(--infinite-cell-color)' }}>
        Current filters:{' '}
        <pre>
          <code>{JSON.stringify(filterValue, null, 2)}</code>
        </pre>
      </div>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          filterValue={filterValue}
          onFilterValueChange={setFilterValue}
          filterDelay={0}
          filterMode="local"
        >
          <InfiniteTable<Developer>
            debugId="onFilterValueChange-example"
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>
  );
};
```

### onLivePaginationCursorChange (`(cursor)=> void`)

> A function to be called when the [`livePaginationCursor`](https://infinite-table.com/docs/reference/datasource-props/index.md#livePaginationCursor) changes.

Also see related [`onDataParamsChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onDataParamsChange).

### onReady (`(dataSourceApi: DataSourceApi<DATA_TYPE>) => void`)

> The callback that is called when the `DataSource` is ready. The [`dataSourceApi`](https://infinite-table.com/docs/reference/datasource-api/index.md) is passed as the first argument.

### onCellSelectionChange (`(cellSelection, selectionMode='multi-cell') => void`)

> A function to be called when the [`cellSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#cellSelection) changes.

**Example: Controlled cell selection with onCellSelectionChange**

Use your mouse to select/deselect cells.

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
  DataSourcePropCellSelection_MultiCell,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 60 },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  age: { field: 'age', type: 'number' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency', type: 'number' },
};

export default function App() {
  const [cellSelection, setCellSelection] =
    React.useState<DataSourcePropCellSelection_MultiCell>({
      defaultSelection: false,
      selectedCells: [
        [3, 'stack'],
        [0, 'firstName'],
      ],
    });

  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <div
        style={{
          maxHeight: 200,
          overflow: 'auto',
          border: '2px solid magenta',
        }}
      >
        Current selection:
        <pre>{JSON.stringify(cellSelection, null, 2)}</pre>
      </div>

      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        cellSelection={cellSelection}
        onCellSelectionChange={setCellSelection}
        selectionMode="multi-cell"
      >
        <InfiniteTable<Developer>
          debugId="controlled-cell-selection-example"
          columns={columns}
          columnDefaultWidth={100}
        />
      </DataSource>
    </div>
  );
}
```

### onRowSelectionChange (`(rowSelection, selectionMode='single-row'|'multi-row') => void`)

> A function to be called when the [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) changes.

**Example: Controlled row selection with onRowSelectionChange**

Use your mouse or keyboard (press the spacebar) to select/deselect a single row.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
  },
  stack: {
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

export default function App() {
  const [rowSelection, setRowSelection] = useState<number | null | string>(3);
  return (
    <>
      <p
        style={{
          color: 'var(--infinite-cell-color)',
          padding: 10,
        }}
      >
        Current row selection:
        <code style={{ display: 'inline-block' }}>
          <pre> {JSON.stringify(rowSelection)}.</pre>
        </code>
      </p>

      <DataSource<Developer>
        data={dataSource}
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="controlled-single-row-selection-example"
          columns={columns}
          columnDefaultWidth={150}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

**Example: Multi row checkbox selection with grouping**

This example shows how you can use multiple row selection with a predefined controlled value.

Go ahead and select some groups/rows and see the selection value adjust.

The example also shows how you can use the `InfiniteTableApi` to retrieve the actual ids of the selected rows.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  InfiniteTableProps,
  InfiniteTablePropColumns,
  DataSourceProps,
  DataSourcePropRowSelection_MultiRow,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
    renderGroupValue: ({ value }) => `Stack: ${value || ''}`,
  },
  age: { field: 'age' },
  id: { field: 'id' },
  preferredLanguage: {
    field: 'preferredLanguage',
    renderGroupValue: ({ value }) => `Lang: ${value || ''}`,
  },
  canDesign: {
    field: 'canDesign',
    renderGroupValue: ({ value }) => `Can design: ${value || ''}`,
  },
};

const defaultGroupBy: DataSourceProps<Developer>['groupBy'] = [
  {
    field: 'canDesign',
  },
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const groupColumn: InfiniteTableProps<Developer>['groupColumn'] = {
  field: 'firstName',
  renderSelectionCheckBox: true,
  defaultWidth: 300,
};

const domProps = {
  style: {
    flex: 1,
    minHeight: 500,
  },
};

export default function App() {
  const [rowSelection, setRowSelection] =
    useState<DataSourcePropRowSelection_MultiRow>({
      selectedRows: [0, 8, 10],
      defaultSelection: false,
    });

  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        overflow: 'auto',
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <div
        style={{
          padding: 10,
        }}
      >
        Current row selection:
        <code
          style={{
            display: 'block',
            height: 100,
            overflow: 'auto',
            border: '1px dashed currentColor',
          }}
        >
          <pre> {JSON.stringify(rowSelection)}.</pre>
        </code>
      </div>

      <DataSource<Developer>
        data={dataSource}
        groupBy={defaultGroupBy}
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="controlled-multi-row-selection-example"
          columns={columns}
          domProps={domProps}
          groupColumn={groupColumn}
          columnDefaultWidth={150}
        />
      </DataSource>
    </div>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

### onSortInfoChange (`(sortInfo | null) => void`)

> Called when sorting changes on the DataSource.

The sorting can change either via a user interaction or by calling an API method (from the [root API](api) or the [Column API](column-api)).

See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) for controlled sorting and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) for uncontrolled sorting.

### refetchKey (`string|number|object`)

> A value that can be used to trigger a re-fetch of the data.

By updating the value of this prop (eg: you can use it as a counter, and increment it) the `<DataSource />` component reloads it's [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) if it's defined as a function. More specifically, the `data` function is called again and the result will replace the current data.

**Example: Re-fetching data via refetchKey updates**

This example shows how you can use the `refetchKey` to trigger reloading the data

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k')
    .then((r) => r.json())
    .then((data) => {
      return new Promise<Developer[]>((resolve) => {
        // add a delay to make "reloading" more visible
        setTimeout(() => {
          resolve(data);
        }, 1000);
      });
    });
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  age: { field: 'age', type: 'number' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency', type: 'number' },
};

export default function App() {
  const [refetchKey, setRefetchKey] = React.useState(0);

  return (
    <>
      <button
        style={{
          color: 'var(--infinite-cell-color)',
          margin: 10,
          padding: 20,
          background: 'var(--infinite-background)',
        }}
        onClick={() => {
          setRefetchKey((k) => k + 1);
        }}
      >
        Refetch data
      </button>
      <DataSource<Developer>
        data={dataSource}
        primaryKey="id"
        refetchKey={refetchKey}
      >
        <InfiniteTable<Developer>
          debugId="refetchKey-example"
          columns={columns}
          columnDefaultWidth={150}
          loadingText={`Refetching with key ${refetchKey}`}
        />
      </DataSource>
    </>
  );
}
```

 

### rowSelection (`string|number|null|object`)

> Describes the selected row(s) in the `DataSource`

For single selection, the prop will be of type: `number | string | null`. Use `null` for empty selection in single selection mode.

For multiple selection, the prop will have the following shape:

```ts
const rowSelection = {
  selectedRows: [3, 6, 100, 23], // those specific rows are selected
  defaultSelection: false, // all rows deselected by default
};

// or
const rowSelection = {
  deselectedRows: [3, 6, 100, 23], // those specific rows are deselected
  defaultSelection: true, // all other rows are selected
};

// or, for grouped data - this example assumes groupBy=continent,country,city
const rowSelection = {
  selectedRows: [
    45, // row with id 45 is selected, no matter the group
    ['Europe', 'France'], // all rows in Europe/France are selected
    ['Asia'], // all rows in Asia are selected
  ],
  deselectedRows: [
    ['Europe', 'France', 'Paris'], // all rows in Paris are deselected
  ],
  defaultSelection: false, // all other rows are selected
};
```

For using group keys in the selection value, see related [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection)

**Example: Single row selection (controlled) with onRowSelectionChange**

Use your mouse or keyboard (press the spacebar) to select/deselect a single row.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
  },
  stack: {
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

export default function App() {
  const [rowSelection, setRowSelection] = useState<number | null | string>(3);
  return (
    <>
      <p
        style={{
          color: 'var(--infinite-cell-color)',
          padding: 10,
        }}
      >
        Current row selection:
        <code style={{ display: 'inline-block' }}>
          <pre> {JSON.stringify(rowSelection)}.</pre>
        </code>
      </p>

      <DataSource<Developer>
        data={dataSource}
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="controlled-single-row-selection-example"
          columns={columns}
          columnDefaultWidth={150}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

When [`lazyLoad`](https://infinite-table.com/docs/reference/datasource-props/index.md#lazyLoad) is being used - this means not all available groups/rows have actually been loaded yet in the dataset - we need a way to allow you to specify that those possibly unloaded rows/groups are selected or not. In this case, the `rowSelection.selectedRows`/`rowSelection.deselectedRows` arrays should not have row primary keys as strings/numbers, but rather rows/groups specified by their full path (so [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) should be set to `true`).

```ts {6}
// this example assumes groupBy=continent,country,city
const rowSelection = {
  selectedRows: [
    // row with id 45 is selected - we need this because in the lazyLoad scenario,
    // not all parents might have been made available yet
    ['Europe','Italy', 'Rome', 45],
    ['Europe','France'], // all rows in Europe/France are selected
    ['Asia'] // all rows in Asia are selected
  ]
  deselectedRows: [
    ['Europe','Italy','Rome'] // all rows in Rome are deselected
    // but note that row with id 45 is selected, so Rome will be rendered with an indeterminate selection state
  ],
  defaultSelection: false // all other rows are selected
}
```

In the example above, we know that there are 3 groups (`continent`, `country`, `city`), so any item in the array that has a 4th element is a fully specified leaf node. While lazy loading, we need this fully specified path for specific nodes, so we know which group rows to render with indeterminate selection.

The [`useGroupKeysForMultiRowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#useGroupKeysForMultiRowSelection) prop can be used for both lazy and non-lazy `DataSource` components.

**Example: Multi row checkbox selection with grouping**

This example shows how you can use multiple row selection with a predefined controlled value.

Go ahead and select some groups/rows and see the selection value adjust.

The example also shows how you can use the `InfiniteTableApi` to retrieve the actual ids of the selected rows.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  InfiniteTableProps,
  InfiniteTablePropColumns,
  DataSourceProps,
  DataSourcePropRowSelection_MultiRow,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
    renderGroupValue: ({ value }) => `Stack: ${value || ''}`,
  },
  age: { field: 'age' },
  id: { field: 'id' },
  preferredLanguage: {
    field: 'preferredLanguage',
    renderGroupValue: ({ value }) => `Lang: ${value || ''}`,
  },
  canDesign: {
    field: 'canDesign',
    renderGroupValue: ({ value }) => `Can design: ${value || ''}`,
  },
};

const defaultGroupBy: DataSourceProps<Developer>['groupBy'] = [
  {
    field: 'canDesign',
  },
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const groupColumn: InfiniteTableProps<Developer>['groupColumn'] = {
  field: 'firstName',
  renderSelectionCheckBox: true,
  defaultWidth: 300,
};

const domProps = {
  style: {
    flex: 1,
    minHeight: 500,
  },
};

export default function App() {
  const [rowSelection, setRowSelection] =
    useState<DataSourcePropRowSelection_MultiRow>({
      selectedRows: [0, 8, 10],
      defaultSelection: false,
    });

  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        overflow: 'auto',
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <div
        style={{
          padding: 10,
        }}
      >
        Current row selection:
        <code
          style={{
            display: 'block',
            height: 100,
            overflow: 'auto',
            border: '1px dashed currentColor',
          }}
        >
          <pre> {JSON.stringify(rowSelection)}.</pre>
        </code>
      </div>

      <DataSource<Developer>
        data={dataSource}
        groupBy={defaultGroupBy}
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="controlled-multi-row-selection-example"
          columns={columns}
          domProps={domProps}
          groupColumn={groupColumn}
          columnDefaultWidth={150}
        />
      </DataSource>
    </div>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```

### selectionMode (`'single-row'|'multi-row'|'multi-cell'|false`)

> Specifies the type of selection that should be enabled.

Read more on row selection (`multi-row` and `single-row`).

Read more on cell selection (`multi-cell` and `single-cell`).

**Example: Choose your selection mode between multi cell or multi row**

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 60 },
  firstName: { field: 'firstName' },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
  country: { field: 'country' },
  age: { field: 'age', type: 'number' },
  salary: { field: 'salary', type: 'number' },
  currency: { field: 'currency', type: 'number' },
};

export default function App() {
  const [selectionMode, setSelectionMode] = React.useState<
    'multi-cell' | 'multi-row'
  >('multi-cell');

  const currentColumns = React.useMemo(() => {
    return {
      ...columns,
      id: {
        field: 'id',
        defaultWidth: 60,
        renderSelectionCheckBox: true,
      },
    } as InfiniteTablePropColumns<Developer>;
  }, [selectionMode]);
  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <p style={{ padding: 10 }}>Please select the selection mode</p>
      <div style={{ padding: 10, paddingTop: 0 }}>
        <select
          style={{
            margin: '10px 0',
            display: 'inline-block',
            background: 'var(--infinite-background)',
            color: 'currentColor',
            padding: 'var(--infinite-space-3)',
          }}
          value={selectionMode}
          onChange={(event) => {
            const selectionMode = event.target.value as
              | 'multi-cell'
              | 'multi-row';

            setSelectionMode(selectionMode);
          }}
        >
          <option value="multi-cell">multi-cell</option>
          <option value="multi-row">multi-row</option>
        </select>
      </div>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        selectionMode={selectionMode}
      >
        <InfiniteTable<Developer>
          debugId="selectionMode-example"
          columns={currentColumns}
          columnDefaultWidth={100}
        />
      </DataSource>
    </div>
  );
}
```

### sortFunction (`(sortInfo:DataSourceSingleSortInfo<T>[], arr: T[]) => T[]`)

> Custom sorting function to replace the `multisort` function used by default.

The function specified in the [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) prop is called with the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) as the first argument and the data array as the second. It should return a sorted array, as per the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) it was called with.

When [`sortFunction`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortFunction) is specified, [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) will be forced to `false`, as the sorting is done in the browser.

The `@infinite-table/infinite-react` package exports a `multisort` function - this is the default function used for local sorting.

```ts
import { multisort } from '@infinite-table/infinite-react';

const arr: Developer[] = [
  /*...*/
];
const sortInfo = [
  {
    field: 'age',
    dir: -1,
  },
  {
    field: 'name',
    dir: 1,
  },
];
multisort(sortInfo, arr);
```

If you want to implement your own custom sort function, the `multisort` fn is a good starting point you can use.

**Example: Using a custom sortFunction**

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceSingleSortInfo,
  multisort,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;

  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const defaultSortInfo: DataSourceSingleSortInfo<Developer> = {
  field: 'stack',
  dir: 1,
};

const sortFunction = (
  sortInfo: DataSourceSingleSortInfo<Developer>[],
  arr: Developer[],
) => {
  // you call the default sorting
  const result = multisort<Developer>(sortInfo, arr);

  // and also apply your custom sorting
  // result.sort((a, b) => {
  // })

  return result;
};
export default function LocalUncontrolledSingleSortingExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        defaultSortInfo={defaultSortInfo}
        sortFunction={sortFunction}
      >
        <InfiniteTable<Developer>
          debugId="local-sortFunction-single-sorting-example-with-local-data-example"
          columns={columns}
          columnDefaultWidth={220}
        />
      </DataSource>
    </>
  );
}

const dataSource: Developer[] = [
  {
    id: 0,

    firstName: 'Nya',
    lastName: 'Klein',
    country: 'India',
    city: 'Unnao',
    age: 24,
    currency: 'JPY',
    preferredLanguage: 'TypeScript',
    stack: 'backend',
    canDesign: 'yes',
    salary: 60000,
    hobby: 'sports',
    email: 'Nya44@gmail.com',
  },
  {
    id: 1,
    firstName: 'Axel',
    lastName: 'Runolfsson',
    country: 'Mexico',
    city: 'Cuitlahuac',
    age: 46,
    currency: 'USD',
    preferredLanguage: 'TypeScript',
    stack: 'backend',
    canDesign: 'no',
    salary: 100000,
    hobby: 'sports',
    email: 'Axel93@hotmail.com',
  },
  {
    id: 2,
    firstName: 'Gonzalo',
    lastName: 'McGlynn',
    country: 'United Arab Emirates',
    city: 'Fujairah',
    age: 54,
    currency: 'JPY',
    preferredLanguage: 'Go',
    stack: 'frontend',
    canDesign: 'yes',
    salary: 120000,
    hobby: 'photography',
    email: 'Gonzalo_McGlynn34@gmail.com',
  },
  {
    id: 3,
    firstName: 'Sherwood',
    lastName: 'McLaughlin',
    country: 'Mexico',
    city: 'Tlacolula de Matamoros',
    age: 43,
    currency: 'CHF',
    preferredLanguage: 'Rust',
    stack: 'backend',
    canDesign: 'no',
    salary: 99000,
    hobby: 'cooking',
    email: 'Sherwood_McLaughlin65@hotmail.com',
  },
  {
    id: 4,
    firstName: 'Alexandre',
    lastName: 'Harber',
    country: 'France',
    city: 'Persan',
    age: 23,
    currency: 'EUR',
    preferredLanguage: 'Go',
    stack: 'backend',
    canDesign: 'yes',
    salary: 97000,
    hobby: 'reading',
    email: 'Alexandre_Harber@hotmail.com',
  },
  {
    id: 5,

    firstName: 'Mariane',
    lastName: 'Schroeder',
    country: 'United States',
    city: 'Hays',
    age: 34,
    currency: 'EUR',
    preferredLanguage: 'TypeScript',
    stack: 'backend',
    canDesign: 'no',
    salary: 58000,
    hobby: 'cooking',
    email: 'Mariane0@hotmail.com',
  },
  {
    id: 6,
    firstName: 'Rosalind',
    lastName: 'Mills',
    country: 'Mexico',
    city: 'Nuevo Casas Grandes',
    age: 33,
    currency: 'AUD',
    preferredLanguage: 'JavaScript',
    stack: 'frontend',
    canDesign: 'no',
    salary: 198000,
    hobby: 'dancing',
    email: 'Rosalind69@gmail.com',
  },
  {
    id: 7,
    firstName: 'Lolita',
    lastName: 'Hayes',
    country: 'Sweden',
    city: 'Delsbo',
    age: 22,
    currency: 'JPY',
    preferredLanguage: 'TypeScript',
    stack: 'full-stack',
    canDesign: 'yes',
    salary: 200000,
    hobby: 'cooking',
    email: 'Lolita.Hayes@hotmail.com',
  },
  {
    id: 8,
    firstName: 'Tre',
    lastName: 'Boyle',
    country: 'Germany',
    city: 'Bad Camberg',
    age: 11,
    currency: 'GBP',
    preferredLanguage: 'TypeScript',
    stack: 'backend',
    canDesign: 'yes',
    salary: 200000,
    hobby: 'sports',
    email: 'Tre28@gmail.com',
  },
  {
    id: 9,
    firstName: 'Lurline',
    lastName: 'Deckow',
    country: 'Canada',
    city: 'Raymore',
    age: 31,
    currency: 'EUR',
    preferredLanguage: 'Rust',
    stack: 'frontend',
    canDesign: 'yes',
    salary: 58000,
    hobby: 'sports',
    email: 'Lurline_Deckow@gmail.com',
  },
];
```

### sortInfo (`DataSourceSingleSortInfo<T>|DataSourceSingleSortInfo<T>[]|null`)

> Information for sorting the data. This is a controlled prop.

Also see related [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo) (uncontrolled version), [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo), [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable) and [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable).

Sorting can be single (only one field/column can be sorted at a time) or multiple (multiple fields/columns can be sorted at the same time). Therefore, this property an be an array of objects or a single object (or null) - the shape of the objects (of type `DataSourceSingleSortInfo<T>`)is the following.

- `dir` - `1 | -1` - the direction of the sorting
- `field`? - `keyof DATA_TYPE` - the field to sort
- `id`? - `string` - if you don't sort by a field, you can specify an id of the column this sorting is bound to. Note that columns have a [valueGetter](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.valueGetter), which will be used when doing local sorting and the column is not bound to an exact field.
- `type` - the sort type - one of the keys in [`sortTypes`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortTypes) - eg `"string"`, `"number"` - will be used for local sorting, to provide the proper comparison function.

When you want to use multiple sorting, but have no default sort order/information, use `[]` (the empty array) to denote multiple sorting should be enabled.

If no `sortInfo` is provided, by default, when clicking a sortable column, single sorting will be applied.

For configuring if a column is sortable or not, see [`columns.sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortable) and [`sortable`](https://infinite-table.com/docs/reference/infinite-table-props.md#sortable). By default, all columns are sortable.

**Example: Remote + controlled multi sorting**

```ts
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  DataSourcePropSortInfo,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = ({ sortInfo }) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }
  const args = [
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');

  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers100-sql?` + args)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function RemoteControlledMultiSortingExample() {
  const [sortInfo, setSortInfo] = React.useState<
    DataSourcePropSortInfo<Developer>
  >([
    {
      field: 'salary',
      dir: -1,
    },
  ]);

  const shouldReloadData = {
    sortInfo: true,
  };
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        sortInfo={sortInfo}
        shouldReloadData={shouldReloadData}
        onSortInfoChange={setSortInfo}
      >
        <InfiniteTable<Developer>
          debugId="remote-controlled-multi-sorting-example"
          columns={columns}
          columnDefaultWidth={220}
        />
      </DataSource>
    </>
  );
}
```

### shouldReloadData.sortInfo (`boolean`)

> Specifies if changes in the [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function. Replaces the deprecated [`sortMode`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortMode).

See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo).

When set to `false` (the default), the data is sorted locally (in the browser) after the data-source is loaded. When set to `true`, the data should be sorted by the server (or by the data-source function that serves the data).

See [the Sorting page](https://infinite-table.com/docs/learn/sorting/overview.md) for more details.

For configuring the sorting behavior when multiple sorting is enabled, see [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior).

### shouldReloadData (`{ sortInfo, groupBy, filterValue, pivotBy }`)

> Specifies which changes in the data-related props should trigger a reload of the data source - applicable when [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) is a function.

See [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo).
See [`shouldReloadData.groupBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.groupBy).
See [`shouldReloadData.filterValue`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.filterValue).
See [`shouldReloadData.pivotBy`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.pivotBy).

### sortMode (`'local'|'remote'`)

> Specifies where the sorting should be done. Use [`shouldReloadData.sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#shouldReloadData.sortInfo) instead.

See related [`sortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#sortInfo) and [`defaultSortInfo`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultSortInfo).

When set to `'local'`, the data is sorted locally (in the browser) after the data-source is loaded. When set to `'remote'`, the data should be sorted by the server (or by the data-source function that serves the data).

See [the Sorting page](https://infinite-table.com/docs/learn/sorting/overview.md) for more details.

For configuring the sorting behavior when multiple sorting is enabled, see [`multiSortBehavior`](https://infinite-table.com/docs/reference/infinite-table-props.md#multiSortBehavior).

### sortTypes (`Record<string, ((a,b) => number)>`)

> Describes the available sorting functions used for local sorting. The object you provide will be merged into the default sort types.

Currently there are two `sortTypes` available:

- `"string"`
- `"number"`
- `"date"`

Those values can be used for the [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) and [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) properties.

```ts
// default implementation
const sortTypes = {
  string: (a, b) => a.localeCompare(b),
  number: (a, b) => a - b,
  date: (a, b) => a - b,
};
```

When a column does not explicitly specify the [column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType), the [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) will be used instead. And if no [column.dataType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.dataType) is defined, it will default to `string`.

You can add new sort types to the DataSource and InfiniteTable components by specifying this property - the object will be merged into the default sort types.

**Example: Custom sort by color - magenta will come first**

```ts
import {
  InfiniteTable,
  DataSource,
  InfiniteTableColumn,
} from '@infinite-table/infinite-react';
import * as React from 'react';

export type CarSale = {
  id: number;

  make: string;
  model: string;
  year: number;

  sales: number;
  color: string;
};

const carsales: CarSale[] = [
  {
    make: 'Volkswagen',
    model: 'GTI',
    year: 2009,
    sales: 6,
    color: 'red',
    id: 0,
  },
  {
    make: 'Honda',
    model: 'Element 2WD',
    year: 2009,
    sales: 739,
    color: 'red',
    id: 1,
  },
  {
    make: 'Acura',
    model: 'RDX 4WD',
    year: 2008,
    sales: 2,
    color: 'magenta',
    id: 2,
  },
  {
    make: 'Honda',
    model: 'Fit',
    year: 2009,
    sales: 211,
    color: 'blue',
    id: 3,
  },
  {
    make: 'Mazda',
    model: '6',
    year: 2009,
    sales: 31,
    color: 'blue',

    id: 4,
  },
  {
    make: 'Acura',
    model: 'TSX',
    year: 2009,
    sales: 14,
    color: 'yellow',
    id: 5,
  },
  {
    make: 'Acura',
    model: 'TSX',
    year: 2010,
    sales: 14,
    color: 'red',
    id: 6,
  },
  {
    make: 'Audi',
    model: 'A3',
    year: 2009,
    sales: 2,
    color: 'magenta',
    id: 7,
  },
];

const columns: Record<string, InfiniteTableColumn<CarSale>> = {
  color: { field: 'color', sortType: 'color' },
  make: { field: 'make' },
  model: { field: 'model' },

  sales: {
    field: 'sales',
    sortType: 'number',
  },
  year: {
    field: 'year',
    sortType: 'number',
  },
};
const newSortTypes = {
  color: (one: string, two: string) => {
    if (one === 'magenta') {
      // magenta comes first
      return -1;
    }
    if (two === 'magenta') {
      // magenta comes first
      return 1;
    }
    return one.localeCompare(two);
  },
};

export default function DataTestPage() {
  return (
    <>
      <DataSource<CarSale>
        data={carsales}
        primaryKey="id"
        defaultSortInfo={{
          field: 'color',
          dir: 1,
          type: 'color',
        }}
        sortTypes={newSortTypes}
      >
        <InfiniteTable<CarSale> debugId="sortTypes-example" columns={columns} />
      </DataSource>
    </>
  );
}
```

In this example, for the `"color"` column, we specified [column.sortType="color"](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) - we could have passed that as `column.dataType` instead, but if the grid had filtering, it wouldn't know what filters to use for "color" - so we used[column.sortType](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.sortType) to only change how the data is sorted.

### useGroupKeysForMultiRowSelection (`boolean`)

> Specifies whether [`rowSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#rowSelection) contains group keys or only row ids/primary keys.

When this is `true`, you might want to use the [getSelectedPrimaryKeys](./selection-api#getSelectedPrimaryKeys) method.

**Example: Multi row checkbox selection using group keys**

This example shows how you can use have row selection with group keys instead of just the primary keys of rows.

```ts
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  InfiniteTableProps,
  InfiniteTableApi,
  InfiniteTablePropColumns,
  DataSourceProps,
  DataSourcePropRowSelection_MultiRow,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useCallback, useRef, useEffect, useState } from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
    renderGroupValue: ({ value }) => `Stack: ${value || ''}`,
  },
  age: { field: 'age' },
  id: { field: 'id' },
  preferredLanguage: {
    field: 'preferredLanguage',
    renderGroupValue: ({ value }) => `Lang: ${value || ''}`,
  },
  canDesign: {
    field: 'canDesign',
    renderGroupValue: ({ value }) => `Can design: ${value || ''}`,
  },
};

const defaultGroupBy: DataSourceProps<Developer>['groupBy'] = [
  {
    field: 'canDesign',
  },
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const groupColumn: InfiniteTableProps<Developer>['groupColumn'] = {
  field: 'firstName',
  renderSelectionCheckBox: true,
  defaultWidth: 300,
};

const domProps = {
  style: {
    flex: 1,
    minHeight: 500,
  },
};

export default function App() {
  const apiRef = useRef<InfiniteTableApi<Developer> | null>(null);
  const [rowSelection, setRowSelection] =
    useState<DataSourcePropRowSelection_MultiRow>({
      selectedRows: [
        ['yes', 'backend', 'TypeScript'],
        ['yes', 'backend', 'Go'],
        16,
        26,
        30,
        ['yes', 'frontend'],
      ],
      deselectedRows: [4, 2],
      defaultSelection: false,
    });

  const [selectedIds, setSelectedIds] = useState<string[]>([]);

  const onReady = useCallback(
    ({ api }: { api: InfiniteTableApi<Developer> }) => {
      apiRef.current = api;

      setSelectedIds(
        api.rowSelectionApi.getSelectedPrimaryKeys(rowSelection) as string[],
      );
    },
    [],
  );

  useEffect(() => {
    if (!apiRef.current) {
      return;
    }
    setSelectedIds(
      apiRef.current.rowSelectionApi.getSelectedPrimaryKeys(
        rowSelection,
      ) as string[],
    );
  }, [rowSelection]);
  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        overflow: 'auto',
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <div
        style={{
          padding: 10,
        }}
      >
        Current row selection:
        <code
          style={{
            display: 'block',
            height: 300,
            overflow: 'auto',
            border: '1px dashed currentColor',
          }}
        >
          <pre> {JSON.stringify(rowSelection, null, 2)}.</pre>
        </code>
        Current selected ids: {selectedIds.join(', ')}
      </div>

      <DataSource<Developer>
        data={dataSource}
        groupBy={defaultGroupBy}
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        useGroupKeysForMultiRowSelection
        primaryKey="id"
      >
        <InfiniteTable<Developer>
          debugId="controlled-multi-row-selection-example-with-group-keys"
          onReady={onReady}
          columns={columns}
          domProps={domProps}
          groupColumn={groupColumn}
          columnDefaultWidth={150}
        />
      </DataSource>
    </div>
  );
}

const dataSource = () => {
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  email: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};
```
