# Lazy loading/Infinite scrolling

CanvasGrid invokes `onViewPortChange` callback whenever user scrolls or pans around the spreadsheet.

This can be used to load data for the next set of visible rows.

{% hint style="info" %}
Do note that lazy loading can affect calculations if cell dependents are out of the viewport and not loaded initially.

We recommend lazy loading only if calculations are moved to the server side.
{% endhint %}

{% code overflow="wrap" %}

```tsx
import { CanvasGrid } from "@rowsncolumns/spreadsheet"

const App = () => {
  const [ sheetData, onChangeSheetData ] = useState<SheetData<T>>({})
  <CanvasGrid
    onViewPortChange={(viewport: ViewPortProps) => {
      const {
        columnStartIndex, 
        columnStopIndex,
        rowStartIndex,
        rowStopIndex,
        visibleColumnStartIndex,
        visibleColumnStopIndex,
        visibleRowStartIndex,
        visibleRowStopIndex      
      } = viewport
      
      // Throttle fetch request
      // Move it out of the render loop, this is just an example
      throttle(
        fetch(`/rows?start=${rowStartIndex}&end=${rowStopIndex}`)
          .then(rowData => {
            // Append new row data
            onChangeSheetData(prev => {
              const newRowData = prev[sheetId]
                  .splice(rowStartIndex, rowStopIndex - rowStartIndex, ...rowData)
              return {
                ...prev,
                [sheetId]: newRowData
              }
            })
          })
      , 300)
    }}
  />
}
```

{% endcode %}

## Hooks for Lazy loading

`@rowsncolumns/spreadsheet-state` exports the `useAsyncDatasource` hook to load paged data for the current viewport while caching nearby pages.

### Usage

```tsx
import { CanvasGrid, CellData, SpreadsheetProvider } from "@rowsncolumns/spreadsheet";
import { RowData, SheetData, useAsyncDatasource } from "@rowsncolumns/spreadsheet-state";
import { CircularLoader } from "@rowsncolumns/ui";

const App = () => {
  const locale = "en-US";
  const [sheetData, onChangeSheetData] = useState<SheetData<CellData>>({});
  const pageSize = 100;
  const rowCount = 100_000;
  const columnCount = 100;
  const sheetId = 1;

  const buildRow = (rowIndex: number): RowData<CellData | null> => {
    const values: (CellData | null)[] = Array.from(
      { length: columnCount + 1 },
      () => null
    );
    values[1] = {
      ue: { sv: `Row ${rowIndex}` },
      fv: `Row ${rowIndex}`,
    };
    values[2] = {
      ue: { nv: rowIndex },
      fv: String(rowIndex),
    };
    values[3] = {
      ue: { sv: `Page ${Math.floor(rowIndex / pageSize)}` },
      fv: `Page ${Math.floor(rowIndex / pageSize)}`,
    };
    return { values };
  };

  const getRowData = useCallback(
    async (_sheetId: number, [rowStartIndex, rowStopIndex]: [number, number]) => {
      await new Promise((res) => setTimeout(res, 200));
      const rows: RowData<CellData | null>[] = [];
      for (let rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {
        rows.push(buildRow(rowIndex));
      }
      return rows;
    },
    [pageSize]
  );

  const { onViewPortChange, isLoading } = useAsyncDatasource<CellData>({
    sheetId,
    locale,
    pageSize,
    bufferPages: 2,
    maxCachedPages: 6,
    rowCount,
    getRowData,
    onChangeSheetData,
  });
  
  return (
    <SpreadsheetProvider>
      <div className="relative flex min-h-[70vh] flex-1">
        <CanvasGrid<CellData>
          sheetId={sheetId}
          rowCount={rowCount}
          columnCount={columnCount}
          onViewPortChange={onViewPortChange}
          getCellData={(targetSheetId, rowIndex, columnIndex) =>
            sheetData?.[targetSheetId]?.[rowIndex]?.values?.[columnIndex]
          }
          readonly
          licenseKey="evaluation-license"
        />
        {isLoading ? (
          <CircularLoader className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10" />
        ) : null}
      </div>
    </SpreadsheetProvider>
  );
}
```

Notes:

* `bufferPages` keeps a cushion of pages around the viewport.
* `maxCachedPages` caps total cached pages and evicts the farthest ones.
* `rowCount` prevents over-fetching beyond your backend total.
