What’s the best way to implement pagination for a questions list in Next.js?

clock icon

asked 65 days ago

message icon

2 Answers

eye icon

39 Views

I have hundreds of questions in my database, and I want to display them in pages of 10 results each. I’m using Next.js 15 (App Router) with server actions. I’m not sure whether to use cursor-based pagination or offset-based pagination for the best performance. My database is PostgreSQL, and I want it to load smoothly when users click “Next” or “Previous.” What are the pros and cons of each approach here?

2 Answers

Best Practices for Pagination in Next.js with PostgreSQL

To implement pagination for a list of questions in Next.js, you need to consider performance, ease of implementation, and user experience. Given your context of using Next.js 15 with server actions and PostgreSQL, you can choose between cursor-based pagination and offset-based pagination. Here’s a breakdown of each method’s pros and cons and a recommended approach for your scenario.

Offset-based Pagination

Pros:

  • Simple to Implement: Straightforward to use, especially for small datasets. Involves using a LIMIT and OFFSET clause in SQL queries.
  • Familiarity: Commonly used and well-understood by most developers.

Cons:

  • Performance Issues: As the offset increases (getting deeper into the dataset), performance can degrade. This is because the database has to count through all previous rows to find where to start the next set.
  • Possible Inaccuracy: If the data is being added or removed, the pages might show duplicates or skip entries as rows shift position.

Cursor-based Pagination

Pros:

  • Performance: More efficient for large datasets. Cursor pagination uses a constant reference point (typically the last object in the previous fetch) to get the next set of records.
  • Accuracy: Reduces the risk of skipping or duplicating records during data updates, since pagination is based on row identity rather than position.

Cons:

  • Complexity: Slightly more complex to implement as it requires keeping track of the cursor.
  • Backend Dependency: Requires that the cursor (commonly an ID or timestamp) be a reliable and consistent column in your database.

Implementation in Next.js with PostgreSQL

For a seamless user experience and optimal performance with large datasets in PostgreSQL, cursor-based pagination is recommended. Here is how you could implement it:

1. Server Action in Next.js:

First, define a server action in your Next.js application. This action will fetch a page of questions based on a cursor:

1export async function loader({ params, request }) {
2 const url = new URL(request.url);
3 const cursor = url.searchParams.get("cursor");
4
5 // SQL query to get questions after the cursor
6 const query = `
7 SELECT * FROM questions
8 WHERE id > $1
9 ORDER BY id ASC
10 LIMIT 10;
11 `;
12 const data = await db.query(query, [cursor || 0]);
13
14 return json(data.rows);
15}
1export async function loader({ params, request }) {
2 const url = new URL(request.url);
3 const cursor = url.searchParams.get("cursor");
4
5 // SQL query to get questions after the cursor
6 const query = `
7 SELECT * FROM questions
8 WHERE id > $1
9 ORDER BY id ASC
10 LIMIT 10;
11 `;
12 const data = await db.query(query, [cursor || 0]);
13
14 return json(data.rows);
15}

2. Client-Side Hook:

Use a React hook on the client side to fetch and display the questions:

1import { useFetcher } from 'next/navigation';
2
3function Questions() {
4 const fetcher = useFetcher();
5 const [cursor, setCursor] = useState(null);
6
7 useEffect(() => {
8 fetcher.load(`/api/questions?cursor=${cursor}`);
9 }, [cursor]);
10
11 return (
12 <div>
13 {fetcher.data?.map(question => (
14 <div key={question.id}>{question.content}</div>
15 ))}
16 <button onClick={() => setCursor(fetcher.data?.[fetcher.data.length - 1]?.id)}>Next</button>
17 </div>
18 );
19}
1import { useFetcher } from 'next/navigation';
2
3function Questions() {
4 const fetcher = useFetcher();
5 const [cursor, setCursor] = useState(null);
6
7 useEffect(() => {
8 fetcher.load(`/api/questions?cursor=${cursor}`);
9 }, [cursor]);
10
11 return (
12 <div>
13 {fetcher.data?.map(question => (
14 <div key={question.id}>{question.content}</div>
15 ))}
16 <button onClick={() => setCursor(fetcher.data?.[fetcher.data.length - 1]?.id)}>Next</button>
17 </div>
18 );
19}

Conclusion

Given the potential size of your dataset and the performance concerns with PostgreSQL, cursor-based pagination offers a more robust and efficient solution. Implementing cursor-based pagination would handle large datasets efficiently and provide a smoother navigation experience as users browse through your questions.

Implementing Pagination in Next.js for a Questions List

Implementing efficient pagination in Next.js with PostgreSQL can greatly enhance the performance and usability of your application. Based on your requirements and setup, we need to decide between cursor-based pagination and offset-based pagination. Let’s explore both options, their pros and cons, and why one might be preferred over the other in your scenario.

Offset-Based Pagination

How it Works:

  • Offset-based pagination uses a simple method where you "skip" a set number of entries (based on the page number) and then fetch the amount of data you want to display on each page (e.g., 10 questions per page).
1SELECT * FROM questions ORDER BY id LIMIT 10 OFFSET 20
1SELECT * FROM questions ORDER BY id LIMIT 10 OFFSET 20

Pros:

  • Simple to implement and understand.
  • Works well with small data sets.

Cons:

  • Performance degrades with large datasets because counting and skipping rows becomes slower as you navigate to higher page numbers.
  • It can lead to inconsistent results if data is added or removed between page views.

Cursor-Based Pagination

How it Works:

  • Cursor-based pagination utilizes a pointer (cursor) to a specific row in a database table. The next set of results is fetched by querying data relative to this cursor's position, usually the last item on the current page.
1SELECT * FROM questions WHERE id > last_seen_id ORDER BY id LIMIT 10
1SELECT * FROM questions WHERE id > last_seen_id ORDER BY id LIMIT 10

Pros:

  • More efficient for large datasets as it does not require skipping over rows.
  • Provides consistent results unaffected by additions or deletions in earlier rows.

Cons:

  • Slightly more complex implementation.
  • Users can’t directly jump to a specific page without first passing through previous pages sequentially.

Recommendation for Your Application

For your case, cursor-based pagination is the advisable choice. Given that you have hundreds of questions in your database and performance is a concern, cursor-based pagination will be more efficient. It avoids the slowdowns associated with the high offset values in offset-based pagination. Furthermore, using cursor-based pagination in conjunction with Next.js's App Router and server actions can optimize your application's data fetching strategies even more effectively.

Implementing Cursor-Based Pagination in Next.js

You can utilize Next.js's server-side capabilities to fetch data:

Step 1: Define the Server Action

Create a server action in Next.js to fetch the data based on the cursor.

1// pages/api/questions.js
2
3export async function loader({ request, params }) {
4 const url = new URL(request.url);
5 const lastSeenId = url.searchParams.get("cursor") || 0;
6 const questions = await db.query(
7 `SELECT * FROM questions WHERE id > $1 ORDER BY id LIMIT 10`,
8 [lastSeenId]
9 );
10 return new Response(JSON.stringify(questions.rows));
11}
1// pages/api/questions.js
2
3export async function loader({ request, params }) {
4 const url = new URL(request.url);
5 const lastSeenId = url.searchParams.get("cursor") || 0;
6 const questions = await db.query(
7 `SELECT * FROM questions WHERE id > $1 ORDER BY id LIMIT 10`,
8 [lastSeenId]
9 );
10 return new Response(JSON.stringify(questions.rows));
11}

Step 2: Fetch Data on the Client

Use the Next.js useFetcher from the App Router to handle data fetching on the client side.

1// pages/questions.js
2import { useFetcher } from 'next/navigation';
3
4export default function QuestionsPage() {
5 const fetcher = useFetcher();
6 const cursor = getLastSeenIdFromCurrentQuestions(); // implement this based on your logic
7
8 useEffect(() => {
9 fetcher.load(`/api/questions?cursor=${cursor}`);
10 }, [cursor]);
11
12 // Handle next and previous buttons
13 // Render questions from fetcher.data
14}
1// pages/questions.js
2import { useFetcher } from 'next/navigation';
3
4export default function QuestionsPage() {
5 const fetcher = useFetcher();
6 const cursor = getLastSeenIdFromCurrentQuestions(); // implement this based on your logic
7
8 useEffect(() => {
9 fetcher.load(`/api/questions?cursor=${cursor}`);
10 }, [cursor]);
11
12 // Handle next and previous buttons
13 // Render questions from fetcher.data
14}

In this setup, you alleviate the potential performance issues associated with large datasets and maintain smoother transitions between pages. Make sure to always handle exceptions and potentially empty results to enhance user experience.

Write your answer here

Top Questions