javascript 135 lines · 4 tabs

Cursor-Paginated REST Endpoint With a React Load More Button

Shared by codesnips Jul 2026
4 tabs
function encodeCursor(row) {
  if (!row) return null;
  const payload = JSON.stringify({ t: row.created_at, id: row.id });
  return Buffer.from(payload, 'utf8').toString('base64url');
}

function decodeCursor(token) {
  if (!token || typeof token !== 'string') return null;
  try {
    const json = Buffer.from(token, 'base64url').toString('utf8');
    const { t, id } = JSON.parse(json);
    if (!t || !id) return null;
    return { createdAt: t, id };
  } catch (err) {
    return null;
  }
}

module.exports = { encodeCursor, decodeCursor };
4 files · javascript Explain with highlit

Cursor pagination replaces the classic LIMIT/OFFSET model with a keyset approach: instead of asking for "page 3", the client asks for rows after a specific record. This avoids the two big problems with offset paging — performance degradation on deep pages (the database still has to scan and discard all skipped rows) and drift, where inserts or deletes between requests cause rows to be skipped or repeated. The trade-off is that clients can no longer jump to an arbitrary page; they can only walk forward from a cursor, which is exactly what a Load More button needs.

In feed.routes.js, the endpoint orders by a stable, unique tuple (created_at, id). The id tie-breaker matters: ordering by created_at alone is ambiguous when timestamps collide, and an ambiguous sort makes the cursor unreliable. The incoming cursor is decoded with decodeCursor, and the query uses a row-value comparison (created_at, id) < (?, ?) so the composite key is compared atomically. It fetches limit + 1 rows as a cheap way to detect whether more data exists without a separate COUNT.

The cursor itself is opaque to the client. In cursor.js, encodeCursor packs the last row's sort fields into base64, and decodeCursor reverses it, returning null for missing or malformed input so a bad cursor degrades to "start from the beginning" rather than throwing. Keeping the cursor opaque means the server can change the underlying sort keys later without breaking clients that treated the token as a black box.

On the client, useCursorPagination owns the paging state: the accumulated items, the current cursor, a hasMore flag, and loading. Its loadMore function guards against concurrent calls with the loading check and appends new items rather than replacing them. Note the AbortController cleanup so an in-flight request from an unmounted component cannot call setState.

Feed.jsx wires the hook to UI: it renders the list and shows the button only when hasMore is true, disabling it while loading. A subtle correctness detail is that the server returns nextCursor as null once the final page is reached, which the hook translates into hasMore = false, so the button disappears exactly when the data runs out. This pattern is the right reach when a feed is append-only or naturally chronological and users rarely need random page access.


Related snips

ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
typescript
import React from "react";

type FallbackProps = {
  error: Error;
  reset: () => void;
};

React Error Boundary + error reporting hook

react frontend error-boundary
by codesnips 3 tabs
javascript
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
  openAnalyzer: true,
});

/** @type {import('next').NextConfig} */

Next.js bundle analyzer for targeted performance work

nextjs performance tooling
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Cursor-Paginated REST Endpoint With a React Load More Button — share card
Link copied