---
title: "Infinite Row Model"
framework: javascript
version: "36.1.0"
---

# Infinite Row Model

> **Note**
>
> If you are an Enterprise user you should consider using the [Server-Side Row Model](https://www.ag-grid.com/javascript-data-grid/server-side-model/) instead of the Infinite Row Model. It offers the same functionality with many more features. The differences between row models can be found in our [row models summary page](https://www.ag-grid.com/javascript-data-grid/row-models/).

Infinite scrolling allows the grid to lazy-load rows from the server depending on what the scroll position is of the grid. In its simplest form, the more the user scrolls down, the more rows get loaded.

The grid will have an 'auto extending' vertical scroll. That means as the scroll reaches the bottom position, the grid will extend the height to allow scrolling even further down, almost making it impossible for the user to reach the bottom. This will stop happening once the grid has extended the scroll to reach the last record in the table.

## How it Works

The grid will ask your application, via a datasource, for the rows in blocks. Each block contains a subset of rows of the entire dataset. The following diagram is a high-level overview.

![high-level](https://www.ag-grid.com/_astro/high-level.BGwchHaO.png)

When the grid scrolls to a position where there is no corresponding block of rows loaded, the model uses the provided datasource to get the rows for the requested block. In the diagram, the datasource is getting the rows from a database in a remote server.

## Turning On Infinite Scrolling

To turn on infinite scrolling, you must a) set the grid property `rowModelType` to `'infinite'` and b) provide a datasource.

```js
// before grid initialised
gridOptions.rowModelType = 'infinite';
gridOptions.datasource = myDataSource;

// after grid initialised, you can set or change the datasource
api.setGridOption('datasource', myDataSource);
```

## Datasource

A datasource must be provided to do infinite scrolling. You specify the datasource as a grid property or using the grid API.

```js
// set as a property
gridOptions.datasource = myDatasource;

// or use the api after the grid is initialised
api.setGridOption('datasource', myDatasource);
```

### Changing the Datasource

Changing the datasource after the grid is initialised will reset the infinite scrolling in the grid. This is useful if the context of your data changes, i.e. if you want to look at a different set of data.

> **Note**
>
> If you call `setGridOption('datasource', datasource)` the grid will act assuming it's a new datasource, resetting the block cache. However you can pass in the same datasource instance. So your application, for example, might have one instance of a datasource that is aware of some external context (e.g. the business date selected for a report, or the 'bank ATM instance' data you are connecting to), and when the context changes, you want to reset, but still keep the same datasource instance. In this case, just call `setGridOption('datasource', datasource)` and pass the same datasource in again.

### Datasource Interface

In a nutshell, every time the grid wants more rows, it will call `getRows()` on the datasource. The datasource responds with the rows requested. Your datasource for infinite scrolling should implement the `IDatasource` interface:

Properties available on the `IDatasource` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `rowCount` | `number` |  |  | If you know up front how many rows are in the dataset, set it here. Otherwise leave blank. |
| `getRows` | `Function` |  |  | Callback the grid calls that you implement to fetch rows from the server. |
| `destroy` | `Function` |  |  | Optional destroy method, if your datasource has state it needs to clean up. |

The `getRows()` method takes the `IGetRowsParams` parameters:

Properties available on the `IGetRowsParams&lt;TData = any, TContext = any&gt;` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `startRow` | `number` |  |  | The first row index to get. |
| `endRow` | `number` |  |  | The first row index to NOT get. |
| `successCallback` | `Function` |  |  | Callback to call for the result when successful. |
| `failCallback` | `Function` |  |  | Callback to call when the request fails. |
| `sortModel` | `SortModelItem[]` |  |  | If doing server side sorting, contains the sort model |
| `filterModel` | `any` |  |  | If doing server side filtering, contains the filter model |
| `api` | [`GridApi`](https://www.ag-grid.com/javascript-data-grid/grid-api/) |  |  | The grid api. |
| `context` | [`TContext`](https://www.ag-grid.com/javascript-data-grid/typescript-generics/#context-tcontext) |  |  | Application context as set on `gridOptions.context`. |

### getRows()

The `getRows()` function is called by the grid to load a block of rows into the browser-side cache of blocks. It takes the following as parameters:

- The `startRow` and `endRow` define the range expected for the call. For example, if the `getRows` function is called with `startRow: 0` and `endRow: 100`, then the grid will expect a result with 100 rows (rows 0 to 99).
- The `successCallback(rowsThisBlock, lastRow)` should be called when you successfully receive data from the server. The callback has the following parameters:
  - `rowsThisBlock` should be the rows you have received for the current block.
  - `lastRow` should be the index of the last row if known.
- The `failCallback()` should be called if the loading failed. Either one of `successCallback()` or `failCallback()` should be called exactly once.
- The `filterModel()` and `sortModel()` are passed for doing server-side sorting and filtering.
- The [context](https://www.ag-grid.com/javascript-data-grid/context/) is just passed as is and nothing to do with infinite scrolling. It's there if you need it for providing application state to your datasource.

### Setting Last Row Index

The success callback parameter `lastRow` is used to move the grid out of infinite scrolling. If the last row is known, then this should be the index of the last row. If the last row is unknown, then leave blank (`undefined`, `null` or `-1`). This attribute is only used when in infinite scrolling. Once the total record count is known, the `lastRow` parameter will be ignored.

Under normal operation, you will return `null` or `undefined` for `lastRow` for every time `getRows()` is called with the exception of when you get to the last block. For example, if block size is 100 and you have 250 rows, when `getRows()` is called for the third time, you will return back 50 rows in the result and set `rowCount` to 250. This will then get the grid to set the scrollbar to fit exactly 250 rows and will not ask for any more blocks.

## Block Cache

The grid keeps the blocks in a cache. You have the choice to never expire the blocks, or to set a limit to the number of blocks kept. If you set a limit, then as you scroll down, previous blocks will be discarded and will be loaded again if the user scrolls back up. The maximum blocks to keep in the cache is set using the `maxBlocksInCache` property.

### Block Size

The block size is set using the grid property `cacheBlockSize`. This is how many rows each block in the cache should contain. Each call to your datasource will be for one block.

### Debounce Block Loading

It is also possible to debounce the loading to prevent blocks loading until scrolling has stopped. This can be configured using: `blockLoadDebounceMillis`.

### Aggregation and Grouping

Aggregation and grouping are not available in infinite scrolling. This is because to do so would require the grid knowing the entire dataset, which is not possible when using the Infinite Row Model. If you need aggregation and / or grouping for large datasets, check the [Server-Side Row Model](https://www.ag-grid.com/javascript-data-grid/server-side-model/) for doing aggregations on the server-side.

### Sorting & Filtering

The grid cannot do sorting or filtering for you, as it does not have all of the data. Sorting or filtering must be done on the server-side. For this reason, if the sort or filter changes, the grid will use the datasource to get the data again and provide the sort and filter state to you.

### Simple Example: No Sorting or Filtering

The example below makes use of infinite scrolling and caching. Notice that the grid will load more data when you bring the scroll all the way to the bottom.

#### Simple Example

```ts
import {
  GridApi,
  GridOptions,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { IOlympicData } from "./interfaces";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([InfiniteRowModelModule]);

let gridApi: GridApi<IOlympicData>;

const gridOptions: GridOptions<IOlympicData> = {
  columnDefs: [
    // this row shows the row index, doesn't use any data from the row
    {
      headerName: "ID",
      maxWidth: 100,
      // it is important to have node.id here, so that when the id changes (which happens
      // when the row is loaded) then the cell is refreshed.
      valueGetter: "node.id",
      cellRenderer: (params: ICellRendererParams) => {
        if (params.value !== undefined) {
          return params.value;
        } else {
          return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
        }
      },
    },
    { field: "athlete", minWidth: 150 },
    { field: "age" },
    { field: "country", minWidth: 150 },
    { field: "year" },
    { field: "date", minWidth: 150 },
    { field: "sport", minWidth: 150 },
    { field: "gold" },
    { field: "silver" },
    { field: "bronze" },
    { field: "total" },
  ],
  defaultColDef: {
    flex: 1,
    minWidth: 100,
    sortable: false,
  },
  rowBuffer: 0,
  // tell grid we want virtual row model type
  rowModelType: "infinite",
  // how big each page in our page cache will be, default is 100
  cacheBlockSize: 100,
  // how many extra blank rows to display to the user at the end of the dataset,
  // which sets the vertical scroll and then allows the grid to request viewing more rows of data.
  // default is 1, ie show 1 row.
  cacheOverflowSize: 2,
  // how many server side requests to send at a time. if user is scrolling lots, then the requests
  // are throttled down
  maxConcurrentDatasourceRequests: 1,
  // how many rows to initially show in the grid. having 1 shows a blank row, so it looks like
  // the grid is loading from the users perspective (as we have a spinner in the first col)
  infiniteInitialRowCount: 1000,
  // how many pages to store in cache. default is undefined, which allows an infinite sized cache,
  // pages are never purged. this should be set for large data to stop your browser from getting
  // full of data
  maxBlocksInCache: 10,

  // debug: true,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll

      getRows: (params: IGetRowsParams) => {
        console.log("asking for " + params.startRow + " to " + params.endRow);

        // At this point in your code, you would call the server.
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const rowsThisPage = data.slice(params.startRow, params.endRow);
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (data.length <= params.endRow) {
            lastRow = data.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });
```

[Live example: Simple Example](https://www.ag-grid.com/examples/infinite-scrolling/simple/typescript)

### Selection

Selection works on the rows in infinite scrolling by using the [Row IDs](https://www.ag-grid.com/javascript-data-grid/row-ids/) of the Row Nodes. If you do not provide Keys for the Row Nodes, the index of the Row Node will be used. Using the index of the row breaks down when (server-side) filtering or sorting, as these change the index of the Rows. For this reason, if you do not provide your own [Row IDs](https://www.ag-grid.com/javascript-data-grid/row-ids/), then selection is cleared if sort or filter is changed.

To provide your own [Row IDs](https://www.ag-grid.com/javascript-data-grid/row-ids/), implement the method `getRowId(params)`, which should return the Key for the data.

```js
gridOptions.getRowId: function(params) {
    // the ID can be any string, as long as it's unique within your dataset
    return params.data.id.toString();
}
```

Once you have `getRowId()` implemented, selection will persist across sorts and filters.

> **Note**
>
> The infinite row model does not have built-in support for [Select-All](https://www.ag-grid.com/javascript-data-grid/row-selection-multi-row/#selecting-all-rows) functionality. Neither clicking the header checkbox nor pressing `^ Ctrl`+A will have any effect.

### Example: Sorting, Filtering and Selection

The following example extends the example above by adding server-side sorting, filtering and persistent selection.

Any column can be sorted by clicking the header. When this happens, the datasource is called again with the new sort options.

The columns `Age`, `Country` and `Year` can be filtered. When this happens, the datasource is called again with the new filtering options.

When a row is selected, the selection will remain inside the grid, even if the grid gets sorted or filtered. Notice that when the grid loads a selected row (e.g. select first row, scroll down so the first block is removed from cache, then scroll back up again) the row is not highlighted until the row is loaded from the server. This is because the grid is waiting to see what the ID is of the row to be loaded.

> **Note**
>
> The example below uses AG Grid Enterprise, to demonstrate the set filter with server-side filtering. AG Grid Enterprise is not required for infinite scrolling.

#### Server-Side Sorting And Filtering

```ts
import {
  ColDef,
  GetRowIdParams,
  GridApi,
  GridOptions,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  NumberFilterModule,
  RowSelectionModule,
  SortModelItem,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  SetFilterModule,
} from "ag-grid-enterprise";
import { getCountries } from "./countries";
import { IOlympicData } from "./interfaces";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  RowSelectionModule,
  ColumnsToolPanelModule,
  InfiniteRowModelModule,
  ColumnMenuModule,
  ContextMenuModule,
  SetFilterModule,
  NumberFilterModule,
]);

const filterParams = { values: getCountries() };

const columnDefs: ColDef[] = [
  // this row just shows the row index, doesn't use any data from the row
  {
    headerName: "ID",
    maxWidth: 100,
    valueGetter: "node.id",
    cellRenderer: (params: ICellRendererParams) => {
      if (params.value !== undefined) {
        return params.value;
      } else {
        return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
      }
    },
    // we don't want to sort by the row index, this doesn't make sense as the point
    // of the row index is to know the row index in what came back from the server
    sortable: false,
    suppressHeaderMenuButton: true,
  },
  { field: "athlete", suppressHeaderMenuButton: true },
  {
    field: "age",
    filter: "agNumberColumnFilter",
    filterParams: {
      filterOptions: ["equals", "lessThan", "greaterThan"],
      maxNumConditions: 1,
    },
  },
  {
    field: "country",
    filter: "agSetColumnFilter",
    filterParams: filterParams,
  },
  {
    field: "year",
    filter: "agSetColumnFilter",
    filterParams: { values: ["2000", "2004", "2008", "2012"] },
  },
  { field: "date" },
  { field: "sport", suppressHeaderMenuButton: true },
  { field: "gold", suppressHeaderMenuButton: true },
  { field: "silver", suppressHeaderMenuButton: true },
  { field: "bronze", suppressHeaderMenuButton: true },
  { field: "total", suppressHeaderMenuButton: true },
];

let gridApi: GridApi<IOlympicData>;

const gridOptions: GridOptions<IOlympicData> = {
  columnDefs: columnDefs,
  defaultColDef: {
    flex: 1,
    minWidth: 150,
    floatingFilter: true,
  },
  rowSelection: { mode: "multiRow", headerCheckbox: false },
  rowModelType: "infinite",
  cacheBlockSize: 100,
  cacheOverflowSize: 2,
  maxConcurrentDatasourceRequests: 2,
  infiniteInitialRowCount: 1,
  maxBlocksInCache: 2,
  getRowId: (params: GetRowIdParams) => {
    return params.data.id;
  },
};

function sortAndFilter(
  allOfTheData: any,
  sortModel: SortModelItem[],
  filterModel: any,
) {
  return sortData(sortModel, filterData(filterModel, allOfTheData));
}

function sortData(sortModel: SortModelItem[], data: any[]) {
  const sortPresent = sortModel && sortModel.length > 0;
  if (!sortPresent) {
    return data;
  }
  // do an in memory sort of the data, across all the fields
  const resultOfSort = data.slice();
  resultOfSort.sort(function (a, b) {
    for (let k = 0; k < sortModel.length; k++) {
      const sortColModel = sortModel[k];
      const valueA = a[sortColModel.colId];
      const valueB = b[sortColModel.colId];
      // this filter didn't find a difference, move onto the next one
      if (valueA == valueB) {
        continue;
      }
      const sortDirection = sortColModel.sort === "asc" ? 1 : -1;
      if (valueA > valueB) {
        return sortDirection;
      } else {
        return sortDirection * -1;
      }
    }
    // no filters found a difference
    return 0;
  });
  return resultOfSort;
}

function filterData(filterModel: any, data: any[]) {
  const filterPresent = filterModel && Object.keys(filterModel).length > 0;
  if (!filterPresent) {
    return data;
  }

  const resultOfFilter = [];
  for (let i = 0; i < data.length; i++) {
    const item = data[i];

    if (filterModel.age) {
      const age = item.age;
      const allowedAge = parseInt(filterModel.age.filter);
      // EQUALS = 1;
      // LESS_THAN = 2;
      // GREATER_THAN = 3;
      if (filterModel.age.type == "equals") {
        if (age !== allowedAge) {
          continue;
        }
      } else if (filterModel.age.type == "lessThan") {
        if (age >= allowedAge) {
          continue;
        }
      } else {
        if (age <= allowedAge) {
          continue;
        }
      }
    }

    if (filterModel.year) {
      if (filterModel.year.values.indexOf(item.year.toString()) < 0) {
        // year didn't match, so skip this record
        continue;
      }
    }

    if (filterModel.country) {
      if (filterModel.country.values.indexOf(item.country) < 0) {
        continue;
      }
    }

    resultOfFilter.push(item);
  }

  return resultOfFilter;
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    // give each row an id
    data.forEach(function (d: any, index: number) {
      d.id = "R" + (index + 1);
    });

    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll

      getRows: (params: IGetRowsParams) => {
        console.log("asking for " + params.startRow + " to " + params.endRow);

        // At this point in your code, you would call the server.
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const dataAfterSortingAndFiltering = sortAndFilter(
            data,
            params.sortModel,
            params.filterModel,
          );
          const rowsThisPage = dataAfterSortingAndFiltering.slice(
            params.startRow,
            params.endRow,
          );
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (dataAfterSortingAndFiltering.length <= params.endRow) {
            lastRow = dataAfterSortingAndFiltering.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });
```

[Live example: Server-Side Sorting And Filtering](https://www.ag-grid.com/examples/infinite-scrolling/server-side/typescript)

> **Note**
>
> When performing multiple row selections using shift-click, it is possible that not all rows are available in memory if the configured value of `maxBlocksInCache` doesn't cover the range. In this case multiple selections will not be allowed.

## Specify Selectable Rows

It is also possible to specify which rows can be selected via the `rowSelection.isRowSelectable` callback function.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `isRowSelectable` | `IsRowSelectable` |  |  | Callback to be used to determine which rows are selectable. By default rows are selectable, so return `false` to make a row non-selectable. |

For instance if we only wanted to allow rows where the `data.country` property is the 'United States' we could implement the following:

```js
const gridOptions = {
    rowSelection: {
        mode: 'multiRow',
        isRowSelectable: function(data) {
            return data.country === 'United States';
        }
    },

    // other grid options ...
}
```

#### Specify Selectable Rows

```ts
import {
  GridApi,
  GridOptions,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  IRowNode,
  InfiniteRowModelModule,
  ModuleRegistry,
  RowSelectionModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { IOlympicData } from "./interfaces";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([RowSelectionModule, InfiniteRowModelModule]);

let gridApi: GridApi<IOlympicData>;

const gridOptions: GridOptions<IOlympicData> = {
  columnDefs: [
    // this row shows the row index, doesn't use any data from the row
    {
      headerName: "ID",
      maxWidth: 100,
      // it is important to have node.id here, so that when the id changes (which happens
      // when the row is loaded) then the cell is refreshed.
      valueGetter: "node.id",
      cellRenderer: (params: ICellRendererParams) => {
        if (params.value !== undefined) {
          return params.value;
        } else {
          return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
        }
      },
    },
    { field: "athlete", minWidth: 200 },
    { field: "age" },
    { field: "country", minWidth: 200 },
    { field: "year" },
    { field: "date", minWidth: 150 },
    { field: "sport", minWidth: 150 },
    { field: "gold" },
    { field: "silver" },
    { field: "bronze" },
    { field: "total" },
  ],
  defaultColDef: {
    flex: 1,
    minWidth: 100,
    sortable: false,
  },
  rowBuffer: 0,
  rowSelection: {
    mode: "multiRow",
    hideDisabledCheckboxes: true,
    headerCheckbox: false,
    isRowSelectable: (rowNode: IRowNode) => {
      return rowNode.data ? rowNode.data.country === "United States" : false;
    },
  },
  // tell grid we want virtual row model type
  rowModelType: "infinite",
  // how big each page in our page cache will be, default is 100
  cacheBlockSize: 100,
  // how many extra blank rows to display to the user at the end of the dataset,
  // which sets the vertical scroll and then allows the grid to request viewing more rows of data.
  // default is 1, ie show 1 row.
  cacheOverflowSize: 2,
  // how many server side requests to send at a time. if user is scrolling lots, then the requests
  // are throttled down
  maxConcurrentDatasourceRequests: 2,
  // how many rows to initially show in the grid. having 1 shows a blank row, so it looks like
  // the grid is loading from the users perspective (as we have a spinner in the first col)
  infiniteInitialRowCount: 1,
  // how many pages to store in cache. default is undefined, which allows an infinite sized cache,
  // pages are never purged. this should be set for large data to stop your browser from getting
  // full of data
  maxBlocksInCache: 2,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll
      getRows: (params: IGetRowsParams) => {
        // console.log('asking for ' + params.startRow + ' to ' + params.endRow);
        // At this point in your code, you would call the server.
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const rowsThisPage = data.slice(params.startRow, params.endRow);
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (data.length <= params.endRow) {
            lastRow = data.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });
```

[Live example: Specify Selectable Rows](https://www.ag-grid.com/examples/infinite-scrolling/specify-selectable-rows/typescript)

Note that in the above example we have hidden disabled checkboxes to help highlight which rows are selectable.

### Configuring a Bit Differently

The examples above use old-style JavaScript objects for the datasource. This example turns things around slightly and creates a datasource Class. The example also just generates data on the fly.

#### Made Up Data

```ts
import {
  ColDef,
  ColumnApiModule,
  GetRowIdParams,
  GridApi,
  GridOptions,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  RowSelectionModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ColumnApiModule,
  RowSelectionModule,
  InfiniteRowModelModule,
]);

const ALPHABET = "abcdefghijklmnopqrstuvwxyz".split("");

function getColumnDefs() {
  const columnDefs: ColDef[] = [
    { headerName: "#", width: 80, valueGetter: "node.rowIndex" },
  ];

  ALPHABET.forEach((letter) => {
    columnDefs.push({
      headerName: letter.toUpperCase(),
      field: letter,
      width: 150,
    });
  });
  return columnDefs;
}

let gridApi: GridApi;

const gridOptions: GridOptions = {
  columnDefs: getColumnDefs(),
  rowModelType: "infinite",
  rowSelection: { mode: "multiRow", headerCheckbox: false },
  maxBlocksInCache: 2,
  getRowId: (params: GetRowIdParams) => {
    return params.data.a;
  },
  datasource: getDataSource(100),
  defaultColDef: {
    sortable: false,
  },
};

function getDataSource(count: number) {
  const dataSource: IDatasource = {
    rowCount: count,
    getRows: (params: IGetRowsParams) => {
      const rowsThisPage: any[] = [];

      for (
        var rowIndex = params.startRow;
        rowIndex < params.endRow;
        rowIndex++
      ) {
        var record: Record<string, string> = {};
        ALPHABET.forEach(function (letter, colIndex) {
          const randomNumber = 17 + rowIndex + colIndex;
          const cellKey = letter.toUpperCase() + (rowIndex + 1);
          record[letter] = cellKey + " = " + randomNumber;
        });
        rowsThisPage.push(record);
      }

      // to mimic server call, we reply after a short delay
      setTimeout(() => {
        // no need to pass the second 'rowCount' parameter as we have already provided it
        params.successCallback(rowsThisPage);
      }, 100);
    },
  };
  return dataSource;
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Made Up Data](https://www.ag-grid.com/examples/infinite-scrolling/made-up-data/typescript)

### Loading Spinner

The examples on this page use a loading spinner to show if the row is waiting for its data to be loaded. The grid does not provide this, rather it is a simple rendering technique used in the examples. If the data is loading, then the `rowNode` will have no ID, so if you use the ID as the value, the cell will get refreshed when the ID is set.

```js
loadingSpinnerColumn = {
    // use a value getter to have the node ID as this column's value
    valueGetter: 'node.id',

    // then a custom cellRenderer
    cellRenderer: function(params) {
        if (params.value === undefined) {
            // when no node id, display the spinner image
            return '<img src="loading.gif" />';
        } else {
            // otherwise just display node ID (or whatever you wish for this column)
            return params.value;
        }
    }
}
```

Refer to section [Cell Rendering](https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/) for how to build cell renderers.

### More Control via Properties and API

Infinite scrolling has a cache working behind the scenes. The following properties and API are provided to give you control of the cache.

### Properties

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `cacheOverflowSize` | `number` |  | `1` | How many extra blank rows to display to the user at the end of the dataset, which sets the vertical scroll and then allows the grid to request viewing more rows of data. Module: [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). [Initial](https://www.ag-grid.com/javascript-data-grid/grid-interface/#initial-grid-options). |
| `maxConcurrentDatasourceRequests` | `number` |  | `2` | How many requests to hit the server with concurrently. If the max is reached, requests are queued. Set to `-1` for no maximum restriction on requests. Modules (any of): [`ServerSideRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). [Initial](https://www.ag-grid.com/javascript-data-grid/grid-interface/#initial-grid-options). |
| `cacheBlockSize` | `number` |  | `100` | How many rows for each block in the store, i.e. how many rows returned from the server at a time. Modules (any of): [`ServerSideRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `maxBlocksInCache` | `number` |  |  | How many blocks to keep in the store. Default is no limit, so every requested block is kept. Use this if you have memory concerns, and blocks that were least recently viewed will be purged when the limit is hit. The grid will additionally make sure it has all the blocks needed to display what is currently visible, in case this property is set to a low value. Modules (any of): [`ServerSideRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). [Initial](https://www.ag-grid.com/javascript-data-grid/grid-interface/#initial-grid-options). |
| `infiniteInitialRowCount` | `number` |  | `1` | How many extra blank rows to display to the user at the end of the dataset, which sets the vertical scroll and then allows the grid to request viewing more rows of data. Module: [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). [Initial](https://www.ag-grid.com/javascript-data-grid/grid-interface/#initial-grid-options). |

### API

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `refreshInfiniteCache` | `Function` |  |  | Marks all the currently loaded blocks in the cache for reload. If you have 10 blocks in the cache, all 10 will be marked for reload. The old data will continue to be displayed until the new data is loaded. Module: [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `purgeInfiniteCache` | `Function` |  |  | Purges the cache. The grid is then told to refresh. Only the blocks required to display the current data on screen are fetched (typically no more than 2). The grid will display nothing while the new blocks are loaded. Use this to immediately remove the old data from the user. Module: [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `isLastRowIndexKnown` | `Function` |  |  | Returns `false` if grid allows for scrolling past the last row to load more rows, thus providing infinite scroll. Modules (any of): [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`ServerSideRowModelApiModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `setRowCount` | `Function` |  |  | Sets the `rowCount` and `maxRowFound` properties. The second parameter, `maxRowFound`, is optional and if left out, only `rowCount` is set. Set `rowCount` to adjust the height of the vertical scroll. Set `maxRowFound` to enable / disable searching for more rows. Use this method if you add or remove rows into the dataset and need to reset the number of rows or instruct the grid that the entire row count is no longer known. Modules (any of): [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`ServerSideRowModelApiModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `getCacheBlockState` | `Function` |  |  | Returns an object representing the state of the cache. This is useful for debugging and understanding how the cache is working. Modules (any of): [`InfiniteRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/), [`ServerSideRowModelApiModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |

> **Note**
>
> Adding / removing rows directly in the grid for infinite scrolling is not recommended as it will complicate your application. It will make your life easier if you update the data on the server and refresh the block cache.

### Example: Using Cache API Methods

Below demonstrates the different API methods via the buttons. The example outputs a lot of debugging items to the console because the grid property `debug=true` is set. The buttons are as follows:

- **Insert Rows**: Inserts 5 rows at row index 2 from the server, then refreshes the grid.
- **Delete Rows**: Deletes 10 rows at row index 3 from the server, then refreshes the grid.
- **Set Row Count**: Sets the row count to 200. This adjusts the vertical scroll to show 200 rows. If the scroll is positioned at the end, this results in the grid automatically re-adjusting as it seeks ahead for the next block of data.
- **Print Info**: Prints `rowCount` and `maxFound` to the console.
- **Jump to 500**: Positions the grid so that row 500 is displayed.
- **Print Cache State**: Debugging method, to see the state of the cache.
- **Set Prices High & Set Prices Low**: Sets the prices on the server-side to either high or low prices. This will not impact the grid until after a block cache is loaded. Use these buttons to then further test the refresh and purge methods.
- **Refresh Cache**: Calls for the cache to be refreshed.
- **Purge Cache**: Calls for the cache to be purged.

The example also makes each Honda row bold - demonstrating that the callbacks `getRowStyle` and `getRowClass` get called after the data is set as well as when the row is created (when the data may not yet be available).

#### Insert And Remove Example

```ts
import {
  ColDef,
  GetRowIdParams,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  RowApiModule,
  RowClassParams,
  RowStyle,
  RowStyleModule,
  ScrollApiModule,
  ValueFormatterParams,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  RowApiModule,
  ScrollApiModule,
  RowStyleModule,
  InfiniteRowModelModule,
]);

const valueFormatter = function (params: ValueFormatterParams) {
  if (typeof params.value === "number") {
    return "£" + params.value.toLocaleString();
  } else {
    return params.value;
  }
};
const columnDefs: ColDef[] = [
  {
    headerName: "Item ID",
    field: "id",
    valueGetter: "node.id",
    cellRenderer: (params: ICellRendererParams) => {
      if (params.value !== undefined) {
        return params.value;
      } else {
        return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
      }
    },
  },
  { field: "make" },
  { field: "model" },
  {
    field: "price",
    valueFormatter: valueFormatter,
  },
];

const datasource: IDatasource = {
  rowCount: undefined, // behave as infinite scroll
  getRows: (params: IGetRowsParams) => {
    console.log("asking for " + params.startRow + " to " + params.endRow);
    // At this point in your code, you would call the server.
    // To make the demo look real, wait for 500ms before returning
    setTimeout(() => {
      // take a slice of the total rows
      const rowsThisPage = allOfTheData.slice(params.startRow, params.endRow);
      // make a copy of each row - this is what would happen if taking data from server
      for (let i = 0; i < rowsThisPage.length; i++) {
        const item = rowsThisPage[i];
        // this is a trick to copy an object
        const itemCopy = JSON.parse(JSON.stringify(item));
        rowsThisPage[i] = itemCopy;
      }
      // if on or after the last page, work out the last row.
      let lastRow = -1;
      if (allOfTheData.length <= params.endRow) {
        lastRow = allOfTheData.length;
      }
      // call the success callback
      params.successCallback(rowsThisPage, lastRow);
    }, 500);
  },
};

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    flex: 1,
    sortable: false,
  },
  columnDefs: columnDefs,
  rowModelType: "infinite",
  datasource: datasource,

  maxBlocksInCache: 2,
  infiniteInitialRowCount: 500,
  maxConcurrentDatasourceRequests: 2,

  getRowId: (params: GetRowIdParams) => {
    return params.data.id.toString();
  },

  onGridReady: (params: GridReadyEvent) => {
    sequenceId = 1;
    allOfTheData = [];
    for (let i = 0; i < 1000; i++) {
      allOfTheData.push(createRowData(sequenceId++));
    }
  },

  getRowStyle: (params: RowClassParams): RowStyle | undefined => {
    if (params.data && params.data.make === "Honda") {
      return {
        fontWeight: "bold",
      };
    }
    return {
      fontWeight: "normal",
    };
  },
};

// this counter is used to give id's to the rows
var sequenceId = 0;
var allOfTheData: any[] = [];

function createRowData(id: number) {
  const makes = ["Toyota", "Ford", "Porsche", "Chevy", "Honda", "Nissan"];
  const models = [
    "Cruze",
    "Celica",
    "Mondeo",
    "Boxster",
    "Genesis",
    "Accord",
    "Taurus",
  ];
  return {
    id: id,
    make: makes[id % makes.length],
    model: models[id % models.length],
    price: 72000,
  };
}

function insertItemsAt2AndRefresh(count: number) {
  insertItemsAt2(count);

  // if the data has stopped looking for the last row, then we need to adjust the
  // row count to allow for the extra data, otherwise the grid will not allow scrolling
  // to the last row. eg if we have 1000 rows, scroll all the way to the bottom (so
  // maxRowFound=true), and then add 5 rows, the rowCount needs to be adjusted
  // to 1005, so grid can scroll to the end. the grid does NOT do this for you in the
  // refreshInfiniteCache() method, as this would be assuming you want to do it which
  // is not true, maybe the row count is constant and you just want to refresh the details.
  const maxRowFound = gridApi!.isLastRowIndexKnown();
  if (maxRowFound) {
    const rowCount = gridApi!.getDisplayedRowCount() || 0;
    gridApi!.setRowCount(rowCount + count);
  }

  // get grid to refresh the data
  gridApi!.refreshInfiniteCache();
}

function insertItemsAt2(count: number) {
  const newDataItems = [];
  for (let i = 0; i < count; i++) {
    const newItem = createRowData(sequenceId++);
    allOfTheData.splice(2, 0, newItem);
    newDataItems.push(newItem);
  }
  return newDataItems;
}

function removeItem(start: number, limit: number) {
  allOfTheData.splice(start, limit);
  gridApi!.refreshInfiniteCache();
}

function refreshCache() {
  gridApi!.refreshInfiniteCache();
}

function purgeCache() {
  gridApi!.purgeInfiniteCache();
}

function setRowCountTo200() {
  gridApi!.setRowCount(200, false);
}

function rowsAndMaxFound() {
  console.log("getDisplayedRowCount() => " + gridApi!.getDisplayedRowCount());
  console.log("isLastRowIndexKnown() => " + gridApi!.isLastRowIndexKnown());
}

// function just gives new prices to the row data, it does not update the grid
function setPricesHigh() {
  allOfTheData.forEach((dataItem) => {
    dataItem.price = Math.round(55500 + 400 * (0.5 + window.agRandom()));
  });
}

function setPricesLow() {
  allOfTheData.forEach((dataItem) => {
    dataItem.price = Math.round(1000 + 100 * (0.5 + window.agRandom()));
  });
}

function jumpTo500() {
  // first up, need to make sure the grid is actually showing 500 or more rows
  if ((gridApi!.getDisplayedRowCount() || 0) < 501) {
    gridApi!.setRowCount(501, false);
  }
  // next, we can jump to the row
  gridApi!.ensureIndexVisible(500);
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).insertItemsAt2AndRefresh = insertItemsAt2AndRefresh;
  (<any>window).removeItem = removeItem;
  (<any>window).refreshCache = refreshCache;
  (<any>window).purgeCache = purgeCache;
  (<any>window).setRowCountTo200 = setRowCountTo200;
  (<any>window).rowsAndMaxFound = rowsAndMaxFound;
  (<any>window).setPricesHigh = setPricesHigh;
  (<any>window).setPricesLow = setPricesLow;
  (<any>window).jumpTo500 = jumpTo500;
}
```

[Live example: Insert And Remove Example](https://www.ag-grid.com/examples/infinite-scrolling/insert-remove/typescript)

## Changing Columns

[Changing columns](https://www.ag-grid.com/javascript-data-grid/column-updating-definitions/) is possible using infinite scroll and it does not require the data getting fetched again from the server. If the change of columns impacts the sort or filter (i.e. a column with a sort or filter applied is removed), the grid will fetch data again similar to how data is fetched again after the user changes the sort or filter explicitly.

The example below demonstrates changing columns on the infinite row model. The following can be noted:

- Hit the buttons 'Show Year' and 'Hide Year'. Notice that the data is not re-fetched.
- Add a sort or filter to Age column. When the sort or filter is applied the data is re-fetched. However once fetched, you can add and remove the Year column without re-fetching the data.
- Add a sort or filter to the Year column. When the sort or filter is applied the data is re-fetched. Now remove the Year column and the data is re-fetched again as the sort or filter has changed.

#### Changing Columns

```ts
import {
  GridApi,
  GridOptions,
  IDatasource,
  InfiniteRowModelModule,
  ModuleRegistry,
  SortModelItem,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { IOlympicData } from "./interfaces";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([TextFilterModule, InfiniteRowModelModule]);

let gridApi: GridApi<IOlympicData>;

const gridOptions: GridOptions<IOlympicData> = {
  columnDefs: [
    { field: "athlete", colId: "athlete", minWidth: 180 },
    { field: "age", colId: "age" },
    { field: "country", colId: "country", minWidth: 180 },
    { field: "year", colId: "year" },
    { field: "sport", colId: "sport", minWidth: 180 },
  ],
  defaultColDef: {
    flex: 1,
    filter: true,
  },
  rowModelType: "infinite",
};

function onBtShowYearColumn() {
  gridApi!.setGridOption("columnDefs", [
    { field: "athlete", colId: "athlete" },
    { field: "age", colId: "age" },
    { field: "country", colId: "country" },
    { field: "year", colId: "year" },
    { field: "sport", colId: "sport" },
  ]);
}

function onBtHideYearColumn() {
  gridApi!.setGridOption("columnDefs", [
    { field: "athlete", colId: "athlete" },
    { field: "age", colId: "age" },
    { field: "country", colId: "country" },
    { field: "sport", colId: "sport" },
  ]);
}

function sortAndFilter(
  allOfTheData: any[],
  sortModel: SortModelItem[],
  filterModel: any,
) {
  return sortData(sortModel, filterData(filterModel, allOfTheData));
}

function sortData(sortModel: SortModelItem[], data: any[]) {
  const sortPresent = sortModel && sortModel.length > 0;
  if (!sortPresent) {
    return data;
  }
  // do an in memory sort of the data, across all the fields
  const resultOfSort = data.slice();
  resultOfSort.sort(function (a, b) {
    for (let k = 0; k < sortModel.length; k++) {
      const sortColModel = sortModel[k];
      const valueA = a[sortColModel.colId];
      const valueB = b[sortColModel.colId];
      // this filter didn't find a difference, move onto the next one
      if (valueA == valueB) {
        continue;
      }
      const sortDirection = sortColModel.sort === "asc" ? 1 : -1;
      if (valueA > valueB) {
        return sortDirection;
      } else {
        return sortDirection * -1;
      }
    }
    // no filters found a difference
    return 0;
  });
  return resultOfSort;
}

function filterData(filterModel: any, data: any[]) {
  const filterPresent = filterModel && Object.keys(filterModel).length > 0;
  if (!filterPresent) {
    return data;
  }

  const resultOfFilter = [];
  for (let i = 0; i < data.length; i++) {
    var item = data[i];

    var filterFails = false;

    const filterKeys = Object.keys(filterModel);
    filterKeys.forEach((filterKey) => {
      const filterValue = filterModel[filterKey].filter;

      const valueForRow = item[filterKey];
      if (filterValue != valueForRow) {
        // year didn't match, so skip this record
        filterFails = true;
      }
    });

    // if (filterModel.year) {
    //     var val1 = filterModel.year.filter;
    //     var val2 = item.year;
    //     if (val1 != val2) {
    //         // year didn't match, so skip this record
    //         continue;
    //     }
    // }
    //

    if (!filterFails) {
      resultOfFilter.push(item);
    }
  }

  return resultOfFilter;
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    // give each row an id
    data.forEach(function (d: any, index: number) {
      d.id = "R" + (index + 1);
    });

    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll
      getRows: (params) => {
        console.log("asking for " + params.startRow + " to " + params.endRow);
        // At this point in your code, you would call the server.
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const dataAfterSortingAndFiltering = sortAndFilter(
            data,
            params.sortModel,
            params.filterModel,
          );
          const rowsThisPage = dataAfterSortingAndFiltering.slice(
            params.startRow,
            params.endRow,
          );
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (dataAfterSortingAndFiltering.length <= params.endRow) {
            lastRow = dataAfterSortingAndFiltering.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).onBtShowYearColumn = onBtShowYearColumn;
  (<any>window).onBtHideYearColumn = onBtHideYearColumn;
}
```

[Live example: Changing Columns](https://www.ag-grid.com/examples/infinite-scrolling/changing-columns/typescript)

## Pagination

As with all row models, it is possible to enable pagination with infinite scrolling. With infinite scrolling, it is possible to mix and match with the configuration to achieve different effects. The following examples are presented:

| Example | Page Size | Block Size | Comment |
| --- | --- | --- | --- |
| Example 1 | Auto | Large | Most Recommended |
| Example 2 | Equal | Equal | Recommended Sometimes |

> **Note**
>
> **Having smaller infinite blocks size than your pagination page size is not supported**
>
> You must have infinite block size greater than or equal to the pagination page size. If you have a smaller block size, the grid will not fetch enough rows to display one page. This breaks how infinite scrolling works and is not supported.

### Example 1: Auto Pagination Page Size, Large Infinite Block Size

This example is the recommended approach. The infinite block size is larger than the pages size, so the grid loads data for a few pages, allowing the user to hit 'next' a few times before a server sided call is needed.

#### Block Larger Than Page

```ts
import {
  ColDef,
  GetRowIdParams,
  GridApi,
  GridOptions,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  NumberFilterModule,
  PaginationModule,
  SortModelItem,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  SetFilterModule,
} from "ag-grid-enterprise";
import { countries } from "./countries";
import { IOlympicData } from "./interfaces";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  PaginationModule,
  ColumnsToolPanelModule,
  InfiniteRowModelModule,
  ColumnMenuModule,
  ContextMenuModule,
  SetFilterModule,
  NumberFilterModule,
]);

const filterParams = { values: countries() };
const columnDefs: ColDef[] = [
  // this row just shows the row index, doesn't use any data from the row
  {
    headerName: "ID",
    maxWidth: 100,
    valueGetter: "node.id",
    cellRenderer: (params: ICellRendererParams) => {
      if (params.value !== undefined) {
        return params.value;
      } else {
        return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
      }
    },
    // we don't want to sort by the row index, this doesn't make sense as the point
    // of the row index is to know the row index in what came back from the server
    sortable: false,
    suppressHeaderMenuButton: true,
  },
  { headerName: "Athlete", field: "athlete", suppressHeaderMenuButton: true },
  {
    field: "age",
    filter: "agNumberColumnFilter",
    filterParams: {
      filterOptions: ["equals", "lessThan", "greaterThan"],
    },
  },
  {
    field: "country",
    filter: "agSetColumnFilter",
    filterParams: filterParams,
  },
  {
    field: "year",
    filter: "agSetColumnFilter",
    filterParams: { values: ["2000", "2004", "2008", "2012"] },
  },
  { field: "date" },
  { field: "sport", suppressHeaderMenuButton: true },
  { field: "gold", suppressHeaderMenuButton: true },
  { field: "silver", suppressHeaderMenuButton: true },
  { field: "bronze", suppressHeaderMenuButton: true },
  { field: "total", suppressHeaderMenuButton: true },
];

let gridApi: GridApi<IOlympicData>;

const gridOptions: GridOptions<IOlympicData> = {
  columnDefs: columnDefs,
  defaultColDef: {
    flex: 1,
    minWidth: 150,
    floatingFilter: true,
  },
  rowModelType: "infinite",
  cacheBlockSize: 100,
  cacheOverflowSize: 2,
  maxConcurrentDatasourceRequests: 2,
  infiniteInitialRowCount: 1,
  maxBlocksInCache: 2,
  pagination: true,
  paginationAutoPageSize: true,
  getRowId: (params: GetRowIdParams) => {
    return params.data.id;
  },
};

function sortAndFilter(
  allOfTheData: any[],
  sortModel: SortModelItem[],
  filterModel: any,
) {
  return sortData(sortModel, filterData(filterModel, allOfTheData));
}

function sortData(sortModel: SortModelItem[], data: any[]) {
  const sortPresent = sortModel && sortModel.length > 0;
  if (!sortPresent) {
    return data;
  }
  // do an in memory sort of the data, across all the fields
  const resultOfSort = data.slice();
  resultOfSort.sort(function (a, b) {
    for (let k = 0; k < sortModel.length; k++) {
      const sortColModel = sortModel[k];
      const valueA = a[sortColModel.colId];
      const valueB = b[sortColModel.colId];
      // this filter didn't find a difference, move onto the next one
      if (valueA == valueB) {
        continue;
      }
      const sortDirection = sortColModel.sort === "asc" ? 1 : -1;
      if (valueA > valueB) {
        return sortDirection;
      } else {
        return sortDirection * -1;
      }
    }
    // no filters found a difference
    return 0;
  });
  return resultOfSort;
}

function filterData(filterModel: any, data: any[]) {
  const filterPresent = filterModel && Object.keys(filterModel).length > 0;
  if (!filterPresent) {
    return data;
  }

  const resultOfFilter = [];
  for (let i = 0; i < data.length; i++) {
    const item = data[i];

    if (filterModel.age) {
      const age = item.age;
      const allowedAge = parseInt(filterModel.age.filter);
      // EQUALS = 1;
      // LESS_THAN = 2;
      // GREATER_THAN = 3;
      if (filterModel.age.type == "equals") {
        if (age !== allowedAge) {
          continue;
        }
      } else if (filterModel.age.type == "lessThan") {
        if (age >= allowedAge) {
          continue;
        }
      } else {
        if (age <= allowedAge) {
          continue;
        }
      }
    }

    if (filterModel.year) {
      if (filterModel.year.values.indexOf(item.year.toString()) < 0) {
        // year didn't match, so skip this record
        continue;
      }
    }

    if (filterModel.country) {
      if (filterModel.country.values.indexOf(item.country) < 0) {
        continue;
      }
    }

    resultOfFilter.push(item);
  }

  return resultOfFilter;
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    data.forEach(function (d: any, index: number) {
      d.id = "R" + (index + 1);
    });

    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll
      getRows: (params: IGetRowsParams) => {
        console.log("asking for " + params.startRow + " to " + params.endRow);
        // At this point in your code, you would call the server.
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const dataAfterSortingAndFiltering = sortAndFilter(
            data,
            params.sortModel,
            params.filterModel,
          );
          const rowsThisPage = dataAfterSortingAndFiltering.slice(
            params.startRow,
            params.endRow,
          );
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (dataAfterSortingAndFiltering.length <= params.endRow) {
            lastRow = dataAfterSortingAndFiltering.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });
```

[Live example: Block Larger Than Page](https://www.ag-grid.com/examples/infinite-scrolling/block-larger-page/typescript)

### Example 2: Equal Pagination Page Size and Large Infinite Block Size

This example demonstrates having the page and block sizes equal. Here the server is hit every time a new page is navigated to.

#### Block Equal Than Page

```ts
import {
  ColDef,
  GetRowIdParams,
  GridApi,
  GridOptions,
  ICellRendererParams,
  IDatasource,
  IGetRowsParams,
  InfiniteRowModelModule,
  ModuleRegistry,
  NumberFilterModule,
  PaginationModule,
  SortModelItem,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  SetFilterModule,
} from "ag-grid-enterprise";
import { countries } from "./countries";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  PaginationModule,
  ColumnsToolPanelModule,
  InfiniteRowModelModule,
  ColumnMenuModule,
  ContextMenuModule,
  SetFilterModule,
  NumberFilterModule,
]);

const filterParams = { values: countries() };
const columnDefs: ColDef[] = [
  // this row just shows the row index, doesn't use any data from the row
  {
    headerName: "ID",
    maxWidth: 100,
    valueGetter: "node.id",
    cellRenderer: (params: ICellRendererParams) => {
      if (params.value !== undefined) {
        return params.value;
      } else {
        return '<img src="https://www.ag-grid.com/example-assets/loading.gif">';
      }
    },
    // we don't want to sort by the row index, this doesn't make sense as the point
    // of the row index is to know the row index in what came back from the server
    sortable: false,
    suppressHeaderMenuButton: true,
  },
  {
    headerName: "Athlete",
    field: "athlete",
    width: 150,
    suppressHeaderMenuButton: true,
  },
  {
    field: "age",
    filter: "agNumberColumnFilter",
    filterParams: {
      filterOptions: ["equals", "lessThan", "greaterThan"],
    },
  },
  {
    field: "country",
    filter: "agSetColumnFilter",
    filterParams: filterParams,
  },
  {
    field: "year",
    filter: "agSetColumnFilter",
    filterParams: { values: ["2000", "2004", "2008", "2012"] },
  },
  { field: "date" },
  { field: "sport", suppressHeaderMenuButton: true },
  { field: "gold", suppressHeaderMenuButton: true },
  { field: "silver", suppressHeaderMenuButton: true },
  { field: "bronze", suppressHeaderMenuButton: true },
  { field: "total", suppressHeaderMenuButton: true },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    flex: 1,
    minWidth: 150,
    floatingFilter: true,
  },
  columnDefs: columnDefs,
  rowModelType: "infinite",
  cacheOverflowSize: 2,
  maxConcurrentDatasourceRequests: 2,
  infiniteInitialRowCount: 1,
  maxBlocksInCache: 2,
  pagination: true,
  getRowId: (params: GetRowIdParams) => {
    return params.data.id;
  },
};

function sortAndFilter(
  allOfTheData: any[],
  sortModel: SortModelItem[],
  filterModel: any,
) {
  return sortData(sortModel, filterData(filterModel, allOfTheData));
}

function sortData(sortModel: SortModelItem[], data: any[]) {
  const sortPresent = sortModel && sortModel.length > 0;
  if (!sortPresent) {
    return data;
  }
  // do an in memory sort of the data, across all the fields
  const resultOfSort = data.slice();
  resultOfSort.sort(function (a, b) {
    for (let k = 0; k < sortModel.length; k++) {
      const sortColModel = sortModel[k];
      const valueA = a[sortColModel.colId];
      const valueB = b[sortColModel.colId];
      // this filter didn't find a difference, move onto the next one
      if (valueA == valueB) {
        continue;
      }
      const sortDirection = sortColModel.sort === "asc" ? 1 : -1;
      if (valueA > valueB) {
        return sortDirection;
      } else {
        return sortDirection * -1;
      }
    }
    // no filters found a difference
    return 0;
  });
  return resultOfSort;
}

function filterData(filterModel: any, data: any[]) {
  const filterPresent = filterModel && Object.keys(filterModel).length > 0;
  if (!filterPresent) {
    return data;
  }

  const resultOfFilter = [];
  for (let i = 0; i < data.length; i++) {
    const item = data[i];

    if (filterModel.age) {
      const age = item.age;
      const allowedAge = parseInt(filterModel.age.filter);
      // EQUALS = 1;
      // LESS_THAN = 2;
      // GREATER_THAN = 3;
      if (filterModel.age.type == "equals") {
        if (age !== allowedAge) {
          continue;
        }
      } else if (filterModel.age.type == "lessThan") {
        if (age >= allowedAge) {
          continue;
        }
      } else {
        if (age <= allowedAge) {
          continue;
        }
      }
    }

    if (filterModel.year) {
      if (filterModel.year.values.indexOf(item.year.toString()) < 0) {
        // year didn't match, so skip this record
        continue;
      }
    }

    if (filterModel.country) {
      if (filterModel.country.values.indexOf(item.country) < 0) {
        continue;
      }
    }

    resultOfFilter.push(item);
  }

  return resultOfFilter;
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then(function (data) {
    // give each row an id
    data.forEach(function (x: any, index: number) {
      x.id = "R" + (index + 1);
    });

    const dataSource: IDatasource = {
      rowCount: undefined, // behave as infinite scroll
      getRows: (params: IGetRowsParams) => {
        console.log("asking for " + params.startRow + " to " + params.endRow);
        // At this point in your code, you would call the server
        // To make the demo look real, wait for 500ms before returning
        setTimeout(() => {
          // take a slice of the total rows
          const dataAfterSortingAndFiltering = sortAndFilter(
            data,
            params.sortModel,
            params.filterModel,
          );
          const rowsThisPage = dataAfterSortingAndFiltering.slice(
            params.startRow,
            params.endRow,
          );
          // if on or after the last page, work out the last row.
          let lastRow = -1;
          if (dataAfterSortingAndFiltering.length <= params.endRow) {
            lastRow = dataAfterSortingAndFiltering.length;
          }
          // call the success callback
          params.successCallback(rowsThisPage, lastRow);
        }, 500);
      },
    };

    gridApi!.setGridOption("datasource", dataSource);
  });
```

[Live example: Block Equal Than Page](https://www.ag-grid.com/examples/infinite-scrolling/block-equal-page/typescript)

## Overlays

The infinite row model does not automatically show the `loading` overlay as rows are loaded in blocks as the user interacts with the grid. The grid will show the `no rows` overlay if the datasource returns an empty array and the `no matching rows` overlay if an empty array is returned while filters are set.

For configuration details, including suppressing the built in overlays, see [Overlays](https://www.ag-grid.com/javascript-data-grid/overlays-overview/).
