How to Implement Infinite Scrolling and Pagination With Next.js and TanStack Query


Most apps you’ll develop will manage data; as programs continue to scale, there can be an increasingly vast amount of it. When applications fail to manage large amounts of data effectively, they perform poorly.


MUO VIDEO OF THE DAY

SCROLL TO CONTINUE WITH CONTENT

Pagination and infinite scrolling are two popular techniques you can use to optimize app performance. They can help you handle data rendering more efficiently and enhance the overall user experience.


Pagination and Infinite Scrolling Using TanStack Query

TanStack Query—an adaptation of React Query—is a robust state management library for JavaScript applications. It offers an efficient solution for managing application state, among other functionalities, including data-related tasks such as caching.

A laptop with code on its screen sitting on a desk

Pagination involves dividing a large dataset into smaller pages, allowing users to navigate the content in manageable chunks using navigation buttons. In contrast, infinite scrolling provides a more dynamic browsing experience. As the user scrolls, new data loads and displays automatically, eliminating the need for explicit navigation.

Pagination and infinite scrolling aim to efficiently manage and present large amounts of data. The choice between the two depends on the application’s data requirements.

You can find this project’s code in this GitHub repository.

Setting Up a Next.js Project

To get started, create a Next.js project. Install the latest version of Next.js 13 which uses the App directory.

 npx create-next-app@latest next-project --app 

Next, install the TanStack package in your project using npm, the Node package manager.

 npm i @tanstack/react-query 

Integrate TanStack Query in the Next.js Application

To integrate TanStack Query in your Next.js project, you need to create and initialize a new instance of TanStack Query in the root of the application—the layout.js file. To do that, import QueryClient and QueryClientProvider from TanStack Query. Then, wrap the children’s prop with QueryClientProvider as follows:

 "use client"
import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({ children }) {
  const queryClient = new QueryClient();

  return (
    <html lang="en">
      <body>
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      </body>
    </html>
  );
}

export { metadata };

This setup ensures that TanStack Query has complete access to the application’s state.

The useQuery hook streamlines data fetching and management. By providing pagination parameters, such as page numbers, you can easily retrieve specific subsets of data.

Additionally, the hook provides various options and configurations to customize your data-fetching functionality, including setting cache options, as well as handling loading states efficiently. With these features, you can effortlessly create a seamless pagination experience.

Now, to implement pagination in the Next.js app, create a Pagination/page.js file in the src/app directory. Inside this file, make the following imports:

 "use client"
import React, { useState } from 'react';
import { useQuery} from '@tanstack/react-query';
import './page.styles.css';

Then, define a React functional component. Inside this component, you need to define a function that will fetch data from an external API. In this case, use the JSONPlaceholder API to fetch a set of posts.

 export default function Pagination() {
  const [page, setPage] = useState(1);

  const fetchPosts = async () => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
                                  _page=${page}&_limit=10`);

      if (!response.ok) {
        throw new Error('Failed to fetch posts');
      }

      const data = await response.json();
      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };

  
}

Now, define the useQuery hook, and specify the following parameters as objects:

   const { isLoading, isError, error, data } = useQuery({
    keepPreviousData: true,
    queryKey: ['posts', page],
    queryFn: fetchPosts,
  });

The keepPreviousData value is true, which ensures that, while fetching new data, the app preserves the previous data. The queryKey parameter is an array containing the key for the query, in this case, the endpoint and the current page you want to fetch data for. Lastly, the queryFn parameter, fetchPosts, triggers the function call to fetch data.

As mentioned earlier, the hook provides several states that you can unpack, similar to how you would destructure arrays and objects, and utilize them to improve the user experience (rendering appropriate UIs) during the data fetching process. These states include isLoading, isError, and more.

To do that, include the following code to render different message screens based on the current state of the ongoing process:

   if (isLoading) {
    return (<h2>Loading...</h2>);
  }

  if (isError) {
    return (<h2 className="error-message">{error.message}</h2>);
  }

Finally, include the code for the JSX elements that will render on the browser page. This code also serves two other functions:

  • Once the app fetches the posts from the API, they will be stored in the data variable provided by the useQuery hook. This variable helps manage the application’s state. You can then map over the list of posts stored in this variable, and render them on the browser.
  • To add two navigation buttons, Previous and Next, to allow users to query and display additional paginated data accordingly.

   return (
    <div>
      <h2 className="header">Next.js Pagination</h2>
      {data && (
        <div className="card">
          <ul className="post-list">
            {data.map((post) => (
                <li key={post.id} className="post-item">{post.title}</li>
            ))}
          </ul>
        </div>
      )}
      <div className='btn-container'>
        <button
          onClick={() => setPage(prevState => Math.max(prevState - 1, 0))}
          disabled={page === 1}
          className="prev-button"
        >Prev Page</button>

        <button
          onClick={() => setPage(prevState => prevState + 1)}
          className="next-button"
        >Next Page</button>
      </div>
    </div>
  );

Lastly, start the development server.

 npm run dev 

Then, head over to http://localhost:3000/Pagination in a browser.

Tanstack Query Pagination Example in Next.js Application

Since you included the Pagination folder in the app directory, Next.js treats it as a route, allowing you to access the page at that URL.

Infinite scrolling provides a seamless browsing experience. A good example is YouTube, which fetches new videos automatically and displays them as you scroll down.

The useInfiniteQuery hook allows you to implement infinite scrolling by fetching data from a server in pages and automatically fetching and rendering the next page of data as the user scrolls down.

To implement infinite scrolling, add an InfiniteScroll/page.js file in the src/app directory. Then, make the following imports:

 "use client"
import React, { useRef, useEffect, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import './page.styles.css';

Next, create a React functional component. Inside this component, similar to the pagination implementation, create a function that will fetch the posts’ data.

 export default function InfiniteScroll() {
  const listRef = useRef(null);
  const [isLoadingMore, setIsLoadingMore] = useState(false);

  const fetchPosts = async ({ pageParam = 1 }) => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?
                                  _page=${pageParam}&_limit=5`);

      if (!response.ok) {
        throw new Error('Failed to fetch posts');
      }

      const data = await response.json();
      await new Promise((resolve) => setTimeout(resolve, 2000));
      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };

  
}

Unlike the pagination implementation, this code introduces a two-second delay when fetching data to allow a user to explore the current data while they scroll to trigger a refetch of a new set of data.

Now, define the useInfiniteQuery hook. When the component initially mounts, the hook will fetch the first page of data from the server. As the user scrolls down, the hook will automatically fetch the next page of data and render it in the component.

   const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < 5) {
        return undefined;
      }
      return allPages.length + 1;
    },
  });

  const posts = data ? data.pages.flatMap((page) => page) : [];

The posts variable combines all the posts from different pages into a single array, resulting in a flattened version of the data variable. This allows you to easily map over and render the individual posts.

To track user scrolls and load more data when the user is close to the bottom of the list, you can define a function that utilizes the Intersection Observer API to detect when elements intersect with the viewport.

   const handleIntersection = (entries) => {
    if (entries[0].isIntersecting && hasNextPage && !isFetching && !isLoadingMore) {
      setIsLoadingMore(true);
      fetchNextPage();
    }
  };

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, { threshold: 0.1 });

    if (listRef.current) {
      observer.observe(listRef.current);
    }

    return () => {
      if (listRef.current) {
        observer.unobserve(listRef.current);
      }
    };
  }, [listRef, handleIntersection]);

  useEffect(() => {
    if (!isFetching) {
      setIsLoadingMore(false);
    }
  }, [isFetching]);

Lastly, include the JSX elements for the posts that render in the browser.

   return (
    <div>
      <h2 className="header">Infinite Scroll</h2>
      <ul ref={listRef} className="post-list">
        {posts.map((post) => (
          <li key={post.id} className="post-item">
            {post.title}
          </li>
        ))}
      </ul>
      <div className="loading-indicator">
        {isFetching ? 'Fetching...' : isLoadingMore ? 'Loading more...' : null}
      </div>
    </div>
  );

Once you have made all the changes, visit http://localhost:3000/InfiniteScroll to see them in action.

TanStack Query: More Than Just Data Fetching

Pagination and infinite scrolling are good examples that highlight the capabilities of TanStack Query. Simply put, it’s an all-around data management library.

With its extensive set of features, you can streamline your app’s data management processes, including efficient handling of state. Alongside other data-related tasks, you can improve the overall performance of your web applications, as well as the user experience.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *