Next.js Functions

 

Implementing Infinite Scrolling in NEXT.js with React Intersection Observer

In today’s web development landscape, creating a seamless user experience is paramount. One way to achieve this is by implementing infinite scrolling, where content loads dynamically as the user scrolls down the page. In this blog post, we’ll explore how to implement infinite scrolling in a NEXT.js application using React Intersection Observer, a powerful tool for handling element visibility in a performant way.

Implementing Infinite Scrolling in NEXT.js with React Intersection Observer

1. What is Infinite Scrolling?

Infinite scrolling, also known as “endless scrolling” or “infinite loading,” is a user interface pattern that allows users to continuously scroll through content without reaching the end of the page. Instead of traditional pagination, where users click through pages, infinite scrolling dynamically loads and appends new content as the user scrolls down, providing a seamless browsing experience.

Popular websites like Facebook, Twitter, and Pinterest employ infinite scrolling to keep users engaged and immersed in their content. Implementing this feature can enhance user retention and create a more enjoyable user experience.

Now, let’s dive into the implementation of infinite scrolling in a NEXT.js application using React Intersection Observer.

2. Setting Up Your NEXT.js Project

Before we start implementing infinite scrolling, we need to set up a NEXT.js project. If you don’t already have NEXT.js installed, you can do so by running the following command:

bash
npx create-next-app infinite-scrolling-demo

This command will create a new NEXT.js project in a directory named infinite-scrolling-demo. You can replace infinite-scrolling-demo with your preferred project name.

Once your project is set up, navigate to the project directory using your terminal:

bash
cd infinite-scrolling-demo

Now, we can move on to installing the necessary packages.

3. Installing React Intersection Observer

To implement infinite scrolling, we need React Intersection Observer, which is a React wrapper for the Intersection Observer API. It allows us to track when an element enters or exits the viewport, making it an excellent choice for triggering actions like loading more content when the user scrolls.

Install React Intersection Observer by running the following command:

bash
npm install react-intersection-observer

With React Intersection Observer installed, we can start creating our infinite scrolling component.

4. Creating a Basic Infinite Scrolling Component

Let’s create a basic component called InfiniteScroll that will serve as the foundation for our infinite scrolling functionality. This component will use React Intersection Observer to detect when the user scrolls to the bottom of the page and trigger a function to load more content.

jsx
import React from 'react';
import { useInView } from 'react-intersection-observer';

const InfiniteScroll = ({ loadMore, hasMore }) => {
  const [ref, inView] = useInView({
    triggerOnce: true,
  });

  React.useEffect(() => {
    if (inView && hasMore) {
      loadMore();
    }
  }, [inView, hasMore, loadMore]);

  return <div ref={ref} />;
};

export default InfiniteScroll;

In this component:

  • We import useInView from react-intersection-observer to track whether the component is in the viewport.
  • We create a ref using useInView to attach to a DOM element.
  • Inside a useEffect, we check if the component is in view and there is more content to load. If both conditions are met, we call the loadMore function.

Now that we have our basic component, let’s integrate it into our NEXT.js application and implement data fetching.

5. Implementing Data Fetching

To demonstrate infinite scrolling, we need some data to load. For this example, we’ll use a simple API to fetch and display a list of items. You can replace this API with your own data source as needed.

Let’s create a service to fetch data. In your project directory, create a folder called services and inside it, create a file named itemService.js with the following code:

javascript
// services/itemService.js

export const fetchItems = async (page, pageSize) => {
  const response = await fetch(
    `https://your-api-url.com/items?page=${page}&pageSize=${pageSize}`
  );

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

  return response.json();
};

This service function fetchItems simulates fetching items from an API with pagination support.

Now, let’s create a component that uses our InfiniteScroll component to load items as the user scrolls. Create a file named ItemsList.js in your project directory:

jsx
// components/ItemsList.js

import React, { useState, useEffect } from 'react';
import InfiniteScroll from './InfiniteScroll';
import { fetchItems } from '../services/itemService';

const ItemsList = () => {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadMore = async () => {
    if (loading) return;
    setLoading(true);

    try {
      const data = await fetchItems(page, 10); // Fetch 10 items per page
      if (data.length === 0) {
        setHasMore(false); // No more items to load
      } else {
        setItems([...items, ...data]);
        setPage(page + 1);
      }
    } catch (error) {
      console.error('Error loading more items:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadMore();
  }, []);

  return (
    <div>
      <h1>Items List</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      {loading && <p>Loading...</p>}
      {!loading && hasMore && <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />}
      {!loading && !hasMore && <p>No more items to load.</p>}
    </div>
  );
};

export default ItemsList;

In this component:

  • We initialize state variables to manage the items, current page, loading state, and whether there are more items to load.
  • The loadMore function is called when the InfiniteScroll component enters the viewport. It fetches more items from the API and appends them to the existing list.
  • We use the useEffect hook to trigger an initial data load when the component mounts.

6. Handling Loading and Error States

To provide a better user experience, let’s handle loading and error states in our ItemsList component. We’ll display loading spinners and error messages as needed.

First, let’s create a loading spinner component. Create a file named LoadingSpinner.js in your components directory:

jsx
// components/LoadingSpinner.js

import React from 'react';

const LoadingSpinner = () => {
  return <div className="spinner" />;
};

export default LoadingSpinner;

Next, update the ItemsList component to use the LoadingSpinner component for the loading state:

jsx
// components/ItemsList.js

import React, { useState, useEffect } from 'react';
import InfiniteScroll from './InfiniteScroll';
import { fetchItems } from '../services/itemService';
import LoadingSpinner from './LoadingSpinner'; // Import the LoadingSpinner component

const ItemsList = () => {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [error, setError] = useState(null); // Add an error state

  const loadMore = async () => {
    if (loading) return;
    setLoading(true);

    try {
      const data = await fetchItems(page, 10);
      if (data.length === 0) {
        setHasMore(false);
      } else {
        setItems([...items, ...data]);
        setPage(page + 1);
      }
    } catch (error) {
      setError(error); // Set the error state
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadMore();
  }, []);

  return (
    <div>
      <h1>Items List</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      {loading && <LoadingSpinner />} {/* Use the LoadingSpinner component */}
      {error && <p>Error: {error.message}</p>} {/* Display error message if there's an error */}
      {!loading && hasMore && <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />}
      {!loading && !hasMore && <p>No more items to load.</p>}
    </div>
  );
};

export default ItemsList;

With these additions, we display a loading spinner when new items are being fetched and show an error message if an error occurs during data retrieval.

7. Adding Style and UX Enhancements

To complete our infinite scrolling implementation, let’s add some style and UX enhancements to make our application more visually appealing. We’ll also include a “Back to Top” button for better navigation.

First, create a CSS file named styles.css in your project’s public directory and add the following styles:

css
/* public/styles.css */

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f0f0f0;
}

.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  font-size: 24px;
  margin-bottom: 20px;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  border: 1px solid #ccc;
  padding: 10px;
  margin-bottom: 10px;
  background-color: #fff;
}

.spinner {
  border: 5px solid #f3f3f3;
  border-top: 5px solid #3498db;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 20px auto;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

#back-to-top {
  display: none;
  position: fixed;
  bottom: 20px;
  right: 20px;
  background-color: #3498db;
  color: #fff;
  border: none;
  border-radius: 50%;
  padding: 10px 15px;
  font-size: 16px;
  cursor: pointer;
}

#back-to-top:hover {
  background-color: #2e86de;
}

This CSS file defines styles for our application, including the loading spinner and the “Back to Top” button.

Next, let’s update our ItemsList component to incorporate these styles and add the “Back to Top” button:

jsx
// components/ItemsList.js

import React, { useState, useEffect, useRef } from 'react';
import InfiniteScroll from './InfiniteScroll';
import { fetchItems } from '../services/itemService';
import LoadingSpinner from './LoadingSpinner';

const ItemsList = () => {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [error, setError] = useState(null);

  const containerRef = useRef(null); // Ref for the container div
  const backToTopRef = useRef(null); // Ref for the "Back to Top" button

  const loadMore = async () => {
    if (loading) return;
    setLoading(true);

    try {
      const data = await fetchItems(page, 10);
      if (data.length === 0) {
        setHasMore(false);
      } else {
        setItems([...items, ...data]);
        setPage(page + 1);
      }
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadMore();
  }, []);

  // Scroll to top when "Back to Top" button is clicked
  const scrollToTop = () => {
    containerRef.current.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  };

  // Show/hide "Back to Top" button based on scroll position
  const handleScroll = () => {
    if (containerRef.current.scrollTop > 100) {
      backToTopRef.current.style.display = 'block';
    } else {
      backToTopRef.current.style.display = 'none';
    }
  };

  useEffect(() => {
    containerRef.current.addEventListener('scroll', handleScroll);

    return () => {
      containerRef.current.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <div className="container" ref={containerRef}>
      <h1>Items List</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      {loading && <LoadingSpinner />}
      {error && <p>Error: {error.message}</p>}
      {!loading && hasMore && <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />}
      {!loading && !hasMore && <p>No more items to load.</p>}
      <button id="back-to-top" onClick={scrollToTop} ref={backToTopRef}>
        &#8593; Top
      </button>
    </div>
  );
};

export default ItemsList;

In this updated component:

  • We use the containerRef to set a ref on the container div, allowing us to control scrolling behavior.
  • The “Back to Top” button is displayed when the user scrolls down, and clicking it smoothly scrolls the user to the top of the page.
  • We add an event listener for the container’s scroll event to control the visibility of the “Back to Top” button.

Conclusion

Congratulations! You’ve successfully implemented infinite scrolling in your NEXT.js application using React Intersection Observer. This feature enhances the user experience by allowing seamless content loading as users scroll through your pages.

Feel free to customize and extend this implementation to suit your project’s specific requirements. Infinite scrolling is a powerful UX feature that can keep users engaged and improve overall user satisfaction.

Now, you have the knowledge and tools to create a dynamic and interactive browsing experience for your users. Happy coding!

Previously at
Flag Argentina
Brazil
time icon
GMT-3
Accomplished Senior Software Engineer with Next.js expertise. 8 years of total experience. Proficient in React, Python, Node.js, MySQL, React Hooks, and more.