Seamless Infinite Scrolling in React: Harnessing the Power of the Observer API

01 Sep 2024
Building-an-infinite-scroll-component-in-react-using-Observer-API
infinite-scrolling-expected-result

Introduction

In this blog I will show you how to implement infinite scrolling in React, a technique that allows users to continuously load content without the need for pagination.This approach reduces clicks and also simplifies navigation.We will see how we can implement this using Observer API.

Prerequisites

  • Basic Knowledge of React
  • Understanding of JavaScript ES6
  • Visual Studio or Visual Studio Code
  • Node.js and npm Installed
  • An account with TMDB (Optional)- It's easy to create an account with TMDB.Just register on their website and get an API KEY.Alternatively, you can use any API that supports pagination pagination

Complete Code My Git hub repo

Step 1: Create React application and install necessary dependencies


            npx create-react-app infinite-scrolling
            npm install intersection-observer
          

Step 2: A quick crash course on setting up API(if you dont have one)

2.1 Register on TMDB Wesbite

Navigate to TMDB website and click on "Join TMDB". Singup TMDB

Choose Username, Password and enter your Email. Click on Singup and then verify your email.

Click on "More" at top navigation bar and choose "API.". It will take you to API. Then click on type of API key you wish to register.

RegisterAPIKey

For our purpose select "Developer". Then it will show you "Terms and conditions" page. Read it and accept if you agree. After this you will be asked to enter application details. Just added a sample below but you need to fill all the details and submit to get the API key

Register Your application

Step 3: Update your code

3.1 App Component


            function App() 
            {
                  const [data, setData] = useState([]);
                  const [page, setPage] = useState(1);
                  const [loading, setLoading] = useState(false);
                  const observerTarget = useRef(null);            
                  useEffect(() => {
                    fetchMovies();
                  }, [page]);

                  //fetchMovies goes and calls TMDB API. Remember to send Authorization header with Bearer {API key}
                  //the result is set to data
                  fetchMoies(){
                    ...API call
                  }
                  return (
                    <div className="App container">
                    {!loading &&
                    data?.map((item) => (
                      <div key="{item.id}" className="col-md-4 mb-4">
                        <MovieComponent item="{item}" />
                      </div>
                    ))}
                  <div
                    ref="observerTarget"
                    className="col-12 text-center"
                    style="{
                      backgroundColor: "orange",
                      color: "black",
                      fontWeight: "Bold",
                    }">
                    Loading
                  </div>
                </div>
                  );
            }
          
Points of interest in above code

observerTarget ref keeps a reference to a DOM element that the IntersectionObserver will observe. This allows the application to detect when the element (here, the loading indicator) comes into view. When the element is visible, the observer triggers a function to load more data (by incrementing the page number that useEffect listens on). Note that currently we don't make an fetchMovies API call yet on load of the page..hang on tight we will see that soon

3.2 MovieComponent

MovieComponent takes in the props that we want to be displayed in our card/component on the front end


              export const MovieComponent = ({ item }) => {
                const { original_name, profile_path, popularity } = item;
                const imageUrl = `https://image.tmdb.org/t/p/w500${profile_path}`;
                return (
                  <div className="card mb-4" style={{ width: "18rem" }}>
                    <img src={imageUrl} className="card-img-top" alt={original_name} />
                    <div className="card-body">
                      <h5 className="card-title">{original_name}</h5>
                      <p className="card-text">popularity is {popularity}</p>
                    </div>
                  </div>
                );
              };
            

3.3 Back in App.js- use ObserverAPI to trigger a fetch

 
            useEffect(() => {
            fetchMovies()  
            const observer = new IntersectionObserver(
              (entries) => {
                if (entries[0].isIntersecting && !loading) {
                
                   setPage((page) => page + 1);
                }
              },
              { threshold: 0.1 }
            );
              if (observerTarget.current) {
              const x = observerTarget.current;
              observer.observe(observerTarget.current);
              console.log("Observing:", observerTarget.current); // Log the observed element
            }
              return () => {
              if (observerTarget.current) {
                observer.unobserve(observerTarget.current);
                console.log("Stopped observing:", observerTarget.current); // Log when stopping observation
              }
            };
          }, []);
Points of interest in above code
  • useEffect hook- It calls the fetchMovies() to load the initial set of movies when the component mounts.
  • Create IntersectionObserver- Creates a new IntersectionObserver instance that watches for visibility changes of the observerTarget element. The observer's callback function checks if the observed element is intersecting (visible in the viewport) and if data is not currently loading. If the observed element is visible and not loading, it increments the page state by 1, triggering a new fetch for more movies
  • Observe Element- If observerTarget.current is defined (i.e., the ref is attached to a DOM element), it starts observing that element.
  • Cleanup- The return function from useEffect is a cleanup function that stops observing the element when the component unmounts or when dependencies change. This prevents memory leaks and ensures that the observer is properly cleaned up.
  • Threshold-1.0 means the observer will only trigger when 100% of the target element is visible. If the loading div is not fully visible, the observer won't trigger. You can also try a lower threshold, like 0.1, to trigger the observer when only a small part of the element is visible.

3.4 How our fetchMovies() looks like


            const fetchMovies = async () => {
              setLoading(true);
              const url = `https://api.themoviedb.org/3/person/popular?language=en-US&page=${page}`;
              const options = {
                method: "GET",
                headers: {
                  accept: "application/json",
                  Authorization:
                    "Bearer {YOUR_API_KEY}",
                },
              };
          
              try {
                const response = await fetch(url, options);
                if (!response.ok) {
                  throw new Error(`Response status: ${response.status}`);
                }
          
                const json = await response.json();
                setData((prevData) => {
                  const newItems = json.results.filter(
                    (item) => !existingIds.has(item.id)
                  );
                  return [...newItems]; // Append only unique new results
                });
              } catch (error) {
                console.error(error.message);
              } finally {
                setLoading(false);
              }
            };
Points of interest in above code
  • Loading State- It sets the loading state to true at the beginning to indicate that data fetching is in progress.
  • API URL- URL that we will use to get data from
  • Options- Defines the options for the fetch request, including the HTTP method (GET) and headers (accepting JSON and including an authorization token).
  • Error Handling- Uses a try-catch block to handle potential errors during the fetch operation
  • Data Processing- If the fetch is successful, it parses the JSON response and appends the new results to the existing data using the setData function
  • Final State Update- In the finally block, it sets the loading state back to false, indicating that the data fetching process has completed, regardless of success or failure.

Conclusion

By following these steps, you have successfully implement infinite scrolling to your react application and provide a great user experience. Complete Code My Git hub repo