How to do pagination for Jira issues

I’m building a Jira Forge App and I need to fetch issues using the Jira Cloud REST API. In the old API, pagination used startAt, maxResults, and total, which made it easy to implement page navigation in the UI:

startAt = the first issue index

maxResults = number of issues per request

total = total number of issues

Now, the new API uses nextPageToken and no longer returns total. The response looks like this:

{
  "issues": [ ... ],
  "maxResults": 100,
  "nextPageToken": "G,ghjir.rtyrky8089kgh.gk"
}

maxResults works as before, e.g., 100 issues per request

nextPageToken is an opaque string used for fetching the next batch

total is no longer available

Problem: I need to implement a traditional page navigation in my Forge App UI (like page 1, 2, 3…), but without total and with the opaque nextPageToken, I cannot calculate the number of pages. Also, nextPageToken is only generated when there are more issues than maxResults.

What I tried:

Looping over requests using nextPageToken works to fetch all issues, but I cannot map it to page numbers in the UI.

Fetching a very large maxResults (e.g., 1000) works, but it may be inefficient.

Question: How can I implement UI pagination in a Jira Forge App using the new Jira Cloud REST API with nextPageToken? Is there a recommended pattern for mapping nextPageToken to page numbers, or should I switch to infinite scroll / lazy loading instead?

Proper pagination for Jira issues is essential because loading too many issues at once on a single page can crash the user’s browser and make the entire application extremely slow, sometimes even unusable.

Additionally, there should be a buffering mechanism so that data not currently visible to the user is not kept in the client’s memory.

This problem becomes especially noticeable when the UI contains many interactive form elements on the same page—such as multiple search fields or input fields—because having too much data loaded at once prevents the user from interacting with the fields properly.

Got it finally work by mothod tell step-by-step instructions for ChatGPT (no one is updated LLM of OpenAI and other AIs to use latest changes to API made by Atlassian).

import api, { route } from “@forge/api”;

export const getIssues = async (payload) => {
const { jql, maxResults = 100, nextPageToken = null } = payload;

const body = {
jql,
maxResults,
nextPageToken
};

const response = await api.asUser().requestJira(
route/rest/api/3/search/jql,
{
method: “POST”,
headers: {
“Accept”: “application/json”,
“Content-Type”: “application/json”
},
body: JSON.stringify(body)
}
);

if (!response.ok) {
throw new Error(Jira API error: ${response.status});
}

const data = await response.json();

return {
issues: data.issues || 
,
nextPageToken: data.nextPageToken || null
};
};
import { useState } from "react";
import { invoke } from "@forge/bridge";

const MAX_RESULTS = 100;

export function IssuesView() {
  const [issues, setIssues] = useState([]);
  const [nextPageToken, setNextPageToken] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchIssues = async () => {
    setLoading(true);

    const response = await invoke("getIssues", {
      jql: "ORDER BY created DESC",
      maxResults: MAX_RESULTS,
      nextPageToken
    });

    setIssues(prev => [...prev, ...response.issues]);
    setNextPageToken(response.nextPageToken);

    setLoading(false);
  };

  return (
    <div>
      <h3>Issues</h3>

      <ul>
        {issues.map(issue => (
          <li key={issue.id}>
            {issue.key} – {issue.fields.summary}
          </li>
        ))}
      </ul>

      {nextPageToken && (
        <button onClick={fetchIssues} disabled={loading}>
          {loading ? "Loading..." : "Next page"}
        </button>
      )}

      {!nextPageToken && issues.length > 0 && (
        <p>No more issues.</p>
      )}
    </div>
  );
}
2 Likes