# Pagination With Cursors — Firebase

Source: https://www.skillbyai.com/en/firebase/qu-page

> startAfter, not offsets.

## Cursor-based pages

Paginate by ordering on a field and using a **query cursor**: keep the last document snapshot of the current page and pass it to `startAfter` for the next page. Cursors are stable when new data arrives and only read the documents you return. Avoid `offset`-style approaches, because skipped documents are still read and billed in Firestore. Order on a field that is unique, or add a tiebreaker such as the document id, so documents with equal values are not skipped or repeated. `startAt`, `endAt` and `endBefore` are also available, plus `limitToLast` for paging backwards.

## Load more

TypeScript.

```typescript
import {
  collection, query, orderBy, limit, startAfter, getDocs,
  QueryDocumentSnapshot, DocumentData,
} from "firebase/firestore";

const PAGE = 20;
let last: QueryDocumentSnapshot<DocumentData> | undefined;

export async function loadMore() {
  const posts = collection(db, "posts");
  const q = last
    ? query(posts, orderBy("createdAt", "desc"), startAfter(last), limit(PAGE))
    : query(posts, orderBy("createdAt", "desc"), limit(PAGE));

  const snap = await getDocs(q);
  last = snap.docs[snap.docs.length - 1] ?? last;
  return { items: snap.docs.map((d) => ({ id: d.id, ...d.data() })), done: snap.size < PAGE };
}
```

## A bookmark, not a page number

You remember the last line you read and continue from there, instead of counting from the start of the book every time.

**Quiz:** Why prefer startAfter over skipping N documents?

- [ ] Cursors bypass Security Rules
- [ ] startAfter works without any ordering
- [ ] Offsets are not allowed in JavaScript
- [x] Skipped documents are still read and billed, while cursors read only the page

*Answer:* Skipped documents are still read and billed, while cursors read only the page. Cursors are cheaper and stable.
