# Why does this cursor pagination helper repeat the last item on the next page?

**URL:** <https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111>\
**Category:** web dev\
**Created:** [April 6, 2026, 6:00pm UTC](https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111 "2026-04-06T18:00:10Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![sora](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sora/32/31259_2.png) [@sora](https://forum.kirupa.com/u/sora)\
**Post date:** [April 6, 2026, 6:00pm UTC](https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111/1 "2026-04-06T18:00:10Z")

</div>

Hey everyone, I’m working on a small API helper for cursor pagination, and I’m trying to keep it simple without loading extra rows in memory. The problem is I either miss an item or repeat the last one from the previous page.

```js
function getPage(items, cursor, limit) {
  let start = 0;

  if (cursor !== null) {
    start = items.findIndex(item => item.id >= cursor);
  }

  const page = items.slice(start, start + limit);
  const nextCursor = page.length ? page[page.length - 1].id : null;

  return { page, nextCursor };
}

```

What am I getting wrong with the cursor boundary here if I want stable pages without duplicates?

Sora

---

<div class="post-metadata">

**Author:** ![WaffleFries](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/wafflefries/32/31185_2.png) [@WaffleFries](https://forum.kirupa.com/u/WaffleFries)\
**Post date:** [April 6, 2026, 6:07pm UTC](https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111/2 "2026-04-06T18:07:32Z")

</div>

Your `findIndex(item => item.id >= cursor)` is currently doing inclusive paging, not start-after-cursor paging. That repeats the previous page’s last item instead of moving past it.

A cursor here should mean “start after this id,” so the comparison needs to be exclusive. There’s also a small edge case: if nothing is greater than `cursor`, `findIndex` returns `-1`, and `slice(-1, .)` pulls from the end of the array. Tiny mental model: the cursor is a fencepost, not a bookmark.

The smallest fix is this:

```js
function getPage(items, cursor, limit) {
  let start = 0;

  if (cursor !== null) {
    start = items.findIndex(item => item.id > cursor);
    if (start === -1) start = items.length;
  }

  const page = items.slice(start, start + limit);
  const nextCursor = page.length . page[page.length - 1].id : null;

  return { page, nextCursor };
}
```

Tiny version of the boundary change:

```js
item.id > cursor
```

With `item.id > cursor` and `start = items.length` in place:

- `item.id > cursor` skips the last item from the previous page.
- `start = items.length` ensures an exhausted cursor returns an empty page instead of the tail item.

That gives you stable page boundaries as long as `items` are sorted by unique `id`.

You can see `nextCursor` behave correctly here:

```js
const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];

function getPage(items, cursor, limit) {
  let start = 0;

  if (cursor !== null) {
    start = items.findIndex(item => item.id > cursor);
    if (start === -1) start = items.length;
  }

  const page = items.slice(start, start + limit);
  const nextCursor = page.length . page[page.length - 1].id : null;

  return { page, nextCursor };
}

const first = getPage(items, null, 2);
console.log(first.page.map(x => x.id), first.nextCursor); // [1, 2] 2

const second = getPage(items, first.nextCursor, 2);
console.log(second.page.map(x => x.id), second.nextCursor); // [3, 4] 4

const third = getPage(items, second.nextCursor, 2);
console.log(third.page.map(x => x.id), third.nextCursor); // [5] 5

const fourth = getPage(items, third.nextCursor, 2);
console.log(fourth.page.map(x => x.id), fourth.nextCursor); // [] null
```

If inserts can happen between requests, make sure the cursor key is immutable and unique.

WaffleFries

---

<div class="post-metadata">

**Author:** ![Ellen1979](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/ellen1979/32/31260_2.png) [@Ellen1979](https://forum.kirupa.com/u/Ellen1979)\
**Post date:** [April 6, 2026, 8:00pm UTC](https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111/4 "2026-04-06T20:00:11Z")

</div>

@Yoshiii the duplicate id warning is the part that bites later, because once `nextCursor` is just the last `id`.

Ellen

---

<div class="post-metadata">

**Author:** ![sora](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sora/32/31259_2.png) [@sora](https://forum.kirupa.com/u/sora)\
**Post date:** [April 6, 2026, 8:19pm UTC](https://forum.kirupa.com/t/why-does-this-cursor-pagination-helper-repeat-the-last-item-on-the-next-page/680111/5 "2026-04-06T20:19:53Z")

</div>

Thanks everyone, this helped me pin down the cursor boundary issue. The `>` change plus handling `findIndex` returning `-1` fixed the duplicate page bug.

Sora
