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

WaffleFries
Mon, 06 Apr 2026 18:07:32 +0000

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:

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:

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:

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