Mastering Data Pagination with Next.js and Server Components

19 Dec 2024 - 6 min read

In today's digital world, applications handle massive amounts of data. Whether it's an e-commerce platform, a social media site, or a blog, managing this data effectively is crucial. Pagination breaks large datasets into smaller, more manageable chunks, improving user experience and application performance. Instead of overwhelming users with excessive data, pagination helps them navigate through it in an organized manner. This approach also boosts performance by loading only a portion of the data at a time, ensuring a responsive application.



Without pagination, loading hundreds of items at once can slow down the application and create a cluttered, confusing interface. Pagination solves this by showing a subset of the data, making the interface cleaner and easier to navigate.


The Challenges of Traditional Client-Side Pagination

Traditionally, React apps used client-side pagination to manage data. This method involved keeping track of the current page, making API calls, and updating the interface dynamically. While effective, it came with challenges. It often required writing a lot of boilerplate code and became harder to maintain as the application scaled. Moreover, client-side pagination could hurt SEO, as search engines struggled to index dynamically rendered content.


While that works, it can lead to performance issues as the app grows, especially when dealing with advanced features like filtering and sorting.


Enter Server Components: Simplifying Pagination

React Server Components, especially when combined with frameworks like Next.js, simplify pagination by fetching data on the server and sending pre-rendered content to the client. This reduces client-side overhead, speeds up the app, and improves SEO by serving fully rendered pages. Server components handle data fetching and logic on the server, which simplifies the process and boosts performance.


A Real-World Implementation of Pagination with Server Components

Here’s how pagination can be implemented with server components in Next.js. This approach ensures optimal performance and cleaner code.


Posts Page Component

The PostsPage component displays posts using the server component PostsList to fetch and render posts, and a client-side Pagination component for navigation.


const PostsPage = async ({ searchParams }: { searchParams: SearchParams }) => {
 const page = parseInt(searchParams.page as string, 10) || 1;

 return (
   <div>
     <h1>Blog Posts</h1>
     <Suspense fallback={<Spinner />}>
       <PostsList page={page} />
     </Suspense>
     <Pagination page={page}/>
   </div>
 );
};


In the PostsPage component, searchParams refers to the query parameters from the URL, which in this case includes the page parameter. This parameter is used to determine the current page of blog posts to display. By extracting the page value from searchParams, the component controls pagination, allowing the user to navigate between different pages. The URL updates accordingly when the user moves between pages, ensuring a clean and SEO-friendly navigation system.


Fetching and Rendering Posts on the Server

The PostsList component fetches posts from the database on the server, reducing client-side overhead.


const PostsList: React.FC<PostsListProps> = async ({ page }) => {
 const posts = await fetchPosts(page);

 if ('error' in posts) {
   return (
     <div>Error loading posts.</div>
   );
 }

return (
   <div>
     <ul>
       {posts.map((post) => (
         <Link key={post.id} href={`/posts/${post.id}`} prefetch={true}>
           <div key={post.id} className="mb-4 w-2/3">
             <h2>{post.title}</h2>
             <p>{post.content}</p>
           </div>
         </Link>
       ))}
     </ul>
   </div>
  )
 };


Database Query for Posts

The fetchPosts function queries the database using Prisma, fetching posts based on the current page.


export const fetchPosts = async (page: number = 1,
): Promise<PostDisplay[] | { error: string }> => {
 try {
   const skip = (page - 1) * POST_LIMIT;
   return await prisma.post.findMany({
     orderBy: {
       createdAt: 'desc',
     },
     skip,
     take: POST_LIMIT,
   });
 } catch (error) {   
	return { error: 'Failed to load posts. Please try again later.' };
 }
};


Client-Side Pagination Navigation

The Pagination component provides navigation controls for the user to move between pages. It updates the URL using Next.js routing to reflect the current page. Notice that within this component, the fetchTotalPages() function is used to retrieve the total number of pages, and it makes an important distinction: fetchTotalPages() is a server action.


const Pagination: React.FC<PaginationProps> = ({ page }) => {
 const [totalPages, setTotalPages] = useState<number>(0);
 const router = useRouter();

 useEffect(() => {
   const getTotalPages = async () => {
     const pages = await fetchTotalPages();
     setTotalPages(pages);
   };

   getTotalPages();
 }, [])

 const pageChange = (newPage: number) => {
   router.push(`/posts?page=${newPage}`);
 };

 return (
   <div>
     <button
       onClick={() => pageChange(page - 1)}
       disabled={page <= 1}
       type="button"
     >
       <FaArrowLeft className="w-4 h-4" />
     </button>

     <p>Page {page} of {' '} {totalPages}</p>

     <button
       onClick={() => pageChange(page + 1)}
       disabled={page >= totalPages}
       type="button"
     >
       <FaArrowRight className="w-4 h-4" />
     </button>
   </div>
 );
};


What are Server Actions

In Next.js and server components, server actions are functions that run on the server, triggered by client-side requests. They handle tasks like data fetching and processing before sending pre-rendered content to the client. This reduces client-side JavaScript execution and improves performance. For instance, fetchTotalPages() could be a server action that calculates the total number of pages based on database records, and once processed on the server, the result is sent to the client to update the UI.


Why Server Components Make Pagination Easier

One of the key benefits of using server components for pagination is that the only thing you need to manage on the client side is the query parameters in the URL. For pagination, this typically involves the page parameter, which indicates the current page number. Once this parameter is updated in the URL, the server takes over—fetching the correct data, rendering the page, and returning it to the client.


Server components ensure faster page loads by fetching data on the server and sending pre-rendered content to the client. This reduces client-side load, improves SEO, and results in a better overall user experience. With server components, managing larger datasets becomes simpler and more efficient.


Conclusion

Pagination is essential for managing large datasets effectively. While traditional client-side pagination had its limitations, server components in React, especially with Next.js, provide a more scalable, maintainable, and SEO-friendly solution. By handling data fetching on the server, applications can deliver faster, more reliable experiences without overloading the client. For a complete implementation, check out the GitHub repository.