# Pagination & infinite scroll

Load data in pages by passing `start` and `count` options to `items()`.

## Key dcupl functions

| Function                               | Purpose                                                       |
| -------------------------------------- | ------------------------------------------------------------- |
| `query.items({ start, count })`        | Get a slice of items                                          |
| `fn.metadata().currentSize`            | Total number of matches (for calculating pages)               |
| `query.applyOptions({ start, count })` | Store the page in the list's query                            |
| `query.count()`                        | Number of items `items()` returns (the page, once one is set) |

## Basic pagination

Pass `start` and `count` to `items()` to load a specific page:

```typescript
// Page 1: items 0-19
const page1 = productList.catalog.query.items({ start: 0, count: 20 });

// Page 2: items 20-39
const page2 = productList.catalog.query.items({ start: 20, count: 20 });

// Page 3: items 40-59
const page3 = productList.catalog.query.items({ start: 40, count: 20 });
```

## Calculate total pages

Use `fn.metadata().currentSize` to determine how many pages exist. It counts every item that matches the current query, even when a page is stored with `applyOptions()` (then `query.count()` returns the length of that page):

```typescript
const PAGE_SIZE = 20;
const totalItems = productList.catalog.fn.metadata().currentSize;
const totalPages = Math.ceil(totalItems / PAGE_SIZE);
```

## Load more (infinite scroll)

Accumulate items as user scrolls:

```typescript
import type { ListItem } from '@dcupl/common';

const PAGE_SIZE = 20;
let currentPage = 0;
let allItems: ListItem[] = [];

function loadMore() {
  const start = currentPage * PAGE_SIZE;
  const newItems = productList.catalog.query.items({ start, count: PAGE_SIZE });

  allItems = [...allItems, ...newItems];
  currentPage++;

  // More items exist while fewer than all matches are loaded
  return allItems.length < productList.catalog.fn.metadata().currentSize;
}

// Filters or data changed: start over, or the loaded pages mix old and new results
productList.onResultChange(() => {
  currentPage = 0;
  allItems = [];
  loadMore();
});
```

Comparing with `currentSize` stops exactly at the end; checking `newItems.length === PAGE_SIZE` would request one more, empty page when the total is a multiple of the page size. Reads with `start` and `count` don't change the list's query, so they don't trigger `onResultChange`.

## Pagination with sorting

Combine pagination with sort options. Sort order values must be uppercase (`'ASC'` / `'DESC'`); with a lowercase value the whole sort is ignored and a warning is logged:

```typescript
type Product = ListItem & { name: string; price: number };

const sortedPage = productList.catalog.query.items<Product>({
  start: 0,
  count: 20,
  sort: {
    attributes: ['price'],
    order: ['ASC'],
  },
});
```

Sorting doesn't change the result type: `items()` returns the list's item type, `Product[]` on a list created with `lists.create<Product>()` and `any[]` on an untyped list. Pass the type to the read (`items<Product>(…)`) to get typed items from an untyped list.

## Reset pagination after filter

When filters change, reset to first page:

```typescript
// Apply new filter
productList.catalog.query.setCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});

// Get filtered first page
const results = productList.catalog.query.items({ start: 0, count: 20 });
```

## Keep the page in the list query

Instead of passing `start` and `count` on every call, store them with `applyOptions()`. `items()` then returns the current page, and `list.onResultChange()` notifies you whenever the page, the filters or the data change. A filter change keeps `start`, so reset it yourself:

```typescript
const PAGE_SIZE = 20;

// Store the first page (other options such as sort are kept)
productList.catalog.query.applyOptions({ start: 0, count: PAGE_SIZE });

// Re-render whenever the result changes
const unsubscribe = productList.onResultChange(() => {
  renderProducts(productList.catalog.query.items());
});

// Next page
productList.catalog.query.applyOptions({ start: PAGE_SIZE });

// New filter: go back to the first page first, then filter
productList.catalog.query.applyOptions({ start: 0 });
productList.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: 'Electronics' });
```

## Complete flow

A paged product list with next/previous buttons, kept in the list's query:

```typescript
import { Dcupl } from '@dcupl/core';
import type { ListItem } from '@dcupl/common';

type Product = ListItem & { name: string; category: string; price: number };

function renderProducts(items: Product[], page: number, pageCount: number) {
  console.log(
    `Page ${page + 1} of ${pageCount}:`,
    items.map((p) => p.name),
  );
}

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'category', type: 'string' },
    { key: 'price', type: 'float' },
  ],
});
dcupl.data.set(
  Array.from({ length: 45 }, (_, i) => ({
    key: `p${i}`,
    name: `Product ${i}`,
    category: i % 3 === 0 ? 'Books' : 'Electronics',
    price: i * 10,
  })),
  { model: 'Product' },
);
await dcupl.init();

const PAGE_SIZE = 20;
const productList = dcupl.lists.create({ modelKey: 'Product' });
let page = 0;

const pageCount = () => Math.ceil(productList.catalog.fn.metadata().currentSize / PAGE_SIZE);

// 1. Re-render whenever the page, the filters or the data change
productList.onResultChange(() => {
  renderProducts(productList.catalog.query.items<Product>(), page, pageCount());
});

// 2. Store the first page in the list's query (renders page 1 of 3)
productList.catalog.query.applyOptions({ start: 0, count: PAGE_SIZE });

// 3. Next / previous page
function goToPage(next: number) {
  const target = Math.max(0, Math.min(next, pageCount() - 1));
  if (target === page) return;
  page = target;
  productList.catalog.query.applyOptions({ start: page * PAGE_SIZE });
}
const nextPage = () => goToPage(page + 1);
const prevPage = () => goToPage(page - 1);

// 4. A new filter starts at the first page again
function filterByCategory(category: string) {
  page = 0;
  // skipProcessing: store the option without recomputing; setCondition() recomputes once
  productList.catalog.query.applyOptions({ start: 0 }, { skipProcessing: true });
  productList.catalog.query.setCondition({ attribute: 'category', operator: 'eq', value: category });
}

nextPage(); // Page 2 of 3
filterByCategory('Books'); // Page 1 of 1 (15 books)
```

## Next steps

- [Query results](/sdk/queries/query-results) — items() and count() reference
- [Query Filter](/sdk/queries/query-filter) — Combine pagination with filters
- [Faceted search](/sdk/advanced/faceted-search) — Add facets to paginated results
