• 18-19 College Green, Dublin 2
  • 01 685 9088
  • info@cunninghamwebsolutions.com
  • cunninghamwebsolutions
    Cunningham Web Solutions
    • Home
    • About Us
    • Our Services
      • Web Design
      • Digital Marketing
      • SEO Services
      • E-commerce Websites
      • Website Redevelopment
      • Social Media Services
    • Digital Marketing
      • Adwords
      • Social Media Services
      • Email Marketing
      • Display Advertising
      • Remarketing
    • Portfolio
    • FAQ’s
    • Blog
    • Contact Us
    MENU CLOSE back  

    Implementing Infinite Scroll And Image Lazy Loading In React

    You are here:
    1. Home
    2. Web Design
    3. Implementing Infinite Scroll And Image Lazy Loading In React

    Implementing Infinite Scroll And Image Lazy Loading In React

    Implementing Infinite Scroll And Image Lazy Loading In React

    Chidi Orji

    2020-03-16T12:00:00+00:00
    2020-03-16T12:35:36+00:00

    If you have been looking for an alternative to pagination, infinite scroll is a good consideration. In this article, we’re going to explore some use cases for the Intersection Observer API in the context of a React functional component. The reader should possess a working knowledge of React functional components. Some familiarity with React hooks will be beneficial but not required, as we will be taking a look at a few.

    Our goal is that at the end of this article, we will have implemented infinite scroll and image lazy loading using a native HTML API. We would also have learned a few more things about React Hooks. With that you can be able to implement infinite scroll and image lazy loading in your React application where necessary.

    Let’s get started.

    Creating Maps With React And Leaflet

    Grasping information from a CSV or a JSON file isn’t only complicated, but is also tedious. Representing the same data in the form of visual aid is simpler. Shajia Abidi explains how powerful of a tool Leaflet is, and how a lot of different kinds of maps can be created. Read article →

    The Intersection Observer API

    According to the MDN docs, “the Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport”.

    This API allows us to implement cool features such as infinite scroll and image lazy loading. The intersection observer is created by calling its constructor and passing it a callback and an options object. The callback is invoked whenever one element, called the target, intersects either the device viewport or a specified element, called the root. We can specify a custom root in the options argument or use the default value.

    let observer = new IntersectionObserver(callback, options);

    The API is straightforward to use. A typical example looks like this:

    var intObserver = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          console.log(entry)
          console.log(entry.isIntersecting) // returns true if the target intersects the root element
        })
      },
      {
        // default options
      }
    );
    let target = document.querySelector('#targetId');
    intObserver.observe(target); // start observation

    entries is a list of IntersectionObserverEntry objects. The IntersectionObserverEntry object describes an intersection change for one observed target element. Note that the callback should not handle any time-consuming task as it runs on the main thread.

    The Intersection Observer API currently enjoys broad browser support, as shown on caniuse.

    Intersection Observer browser support. (Large preview)

    You can read more about the API in the links provided in the resources section.

    Let us now look at how to make use of this API in a real React app. The final version of our app will be a page of pictures that scrolls infinitely and will have each image loaded lazily.

    Making API Calls With The useEffect Hook

    To get started, clone the starter project from this URL. It has minimal setup and a few styles defined. I’ve also added a link to Bootstrap‘s CSS in the public/index.html file as I’ll be using its classes for styling.

    Feel free to create a new project if you like. Make sure you have yarn package manager installed if you want to follow with the repo. You can find the installation instructions for your specific operating system here.

    For this tutorial, we’re going to be grabbing pictures from a public API and displaying them on the page. We will be using the Lorem Picsum APIs.

    For this tutorial, we’ll be using the endpoint, https://picsum.photos/v2/list?page=0&limit=10, which returns an array of picture objects. To get the next ten pictures, we change the value of page to 1, then 2, and so on.

    We will now build the App component piece by piece.

    Open up src/App.js and enter the following code.

    import React, { useEffect, useReducer } from 'react';
    
    import './index.css';
    
    function App() {
      const imgReducer = (state, action) => {
        switch (action.type) {
          case 'STACK_IMAGES':
            return { ...state, images: state.images.concat(action.images) }
          case 'FETCHING_IMAGES':
            return { ...state, fetching: action.fetching }
          default:
            return state;
        }
      }
      const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true})
      // next code block goes here
    }

    Firstly, we define a reducer function, imgReducer. This reducer handles two actions.

    1. The STACK_IMAGES action concatenates the images array.
    2. FETCHING_IMAGES action toggles the value of the fetching variable between true and false.

    The next step is to wire up this reducer to a useReducer hook. Once that is done, we get back two things:

    1. imgData, which contains two variables: images is the array of picture objects. fetching is a boolean which tells us if the API call is in progress or not.
    2. imgDispatch, which is a function for updating the reducer object.

    You can learn more about the useReducer hook in the React documentation.

    The next part of the code is where we make the API call. Paste the following code below the previous code block in App.js.

    // make API calls
    useEffect(() => {
      imgDispatch({ type: 'FETCHING_IMAGES', fetching: true })
      fetch('https://picsum.photos/v2/list?page=0&limit=10')
        .then(data => data.json())
        .then(images => {
          imgDispatch({ type: 'STACK_IMAGES', images })
          imgDispatch({ type: 'FETCHING_IMAGES', fetching: false })
        })
        .catch(e => {
          // handle error
          imgDispatch({ type: 'FETCHING_IMAGES', fetching: false })
          return e
        })
    }, [ imgDispatch ])
    
    // next code block goes here

    Inside the useEffect hook, we make a call to the API endpoint with fetch API. We then update the images array with the result of the API call by dispatching the STACK_IMAGES action. We also dispatch the FETCHING_IMAGES action once the API call completes.

    The next block of code defines the return value of the function. Enter the following code after the useEffect hook.

    return (
      <div className="">
        <nav className="navbar bg-light">
          <div className="container">
            <a className="navbar-brand" href="/#">
              <h2>Infinite scroll + image lazy loading</h2>
            </a>
          </div>
        </navv
        <div id='images' className="container">
          <div className="row">
            {imgData.images.map((image, index) => {
              const { author, download_url } = image
              return (
                <div key={index} className="card">
                  <div className="card-body ">
                    <img
                      alt={author}
                      className="card-img-top"
                      src={download_url}
                    />
                  </div>
                  <div className="card-footer">
                    <p className="card-text text-center text-capitalize text-primary">Shot by: {author}</p>
                  </div>
                </div>
              )
            })}
          </div>
        </div>
      </div>
    );

    To display the images, we map over the images array in the imgData object.

    Now start the app and view the page in the browser. You should see the images nicely displayed in a responsive grid.

    The last bit is to export the App component.

    export default App;

    Pictures in responsive grid. (Large preview)

    The corresponding branch at this point is 01-make-api-calls.

    Let’s now extend this by displaying more pictures as the page scrolls.

    Implementing Infinite Scroll

    We aim to present more pictures as the page scrolls. From the URL of the API endpoint, https://picsum.photos/v2/list?page=0&limit=10, we know that to get a new set of photos, we only need to increment the value of page. We also need to do this when we have run out of pictures to show. For our purpose here, we’ll know we have run out of images when we hit the bottom of the page. It’s time to see how the Intersection Observer API helps us achieve that.

    Open up src/App.js and create a new reducer, pageReducer, below imgReducer.

    // App.js
    const imgReducer = (state, action) => {
      ...
    }
    const pageReducer = (state, action) => {
      switch (action.type) {
        case 'ADVANCE_PAGE':
          return { ...state, page: state.page + 1 }
        default:
          return state;
      }
    }
    const [ pager, pagerDispatch ] = useReducer(pageReducer, { page: 0 })

    We define only one action type. Each time the ADVANCE_PAGE action is triggered, the value of page is incremented by 1.

    Update the URL in the fetch function to accept page numbers dynamically as shown below.

    fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)

    Add pager.page to the dependency array alongside imgData. Doing this ensures that the API call will run whenever pager.page changes.

    useEffect(() => {
    ...
    }, [ imgDispatch, pager.page ])

    After the useEffect hook for the API call, enter the below code. Update your import line as well.

    // App.js
    import React, { useEffect, useReducer, useCallback, useRef } from 'react';
    useEffect(() => {
      ...
    }, [ imgDispatch, pager.page ])
    
    // implement infinite scrolling with intersection observer
    let bottomBoundaryRef = useRef(null);
    const scrollObserver = useCallback(
      node => {
        new IntersectionObserver(entries => {
          entries.forEach(en => {
            if (en.intersectionRatio > 0) {
              pagerDispatch({ type: 'ADVANCE_PAGE' });
            }
          });
        }).observe(node);
      },
      [pagerDispatch]
    );
    useEffect(() => {
      if (bottomBoundaryRef.current) {
        scrollObserver(bottomBoundaryRef.current);
      }
    }, [scrollObserver, bottomBoundaryRef]);

    We define a variable bottomBoundaryRef and set its value to useRef(null). useRef lets variables preserve their values across component renders, i.e. the current value of the variable persists when the containing component re-renders. The only way to change its value is by re-assigning the .current property on that variable.

    In our case, bottomBoundaryRef.current starts with a value of null. As the page rendering cycle proceeds, we set its current property to be the node

    .

    We use the assignment statement ref={bottomBoundaryRef} to tell React to set bottomBoundaryRef.current to be the div where this assignment is declared.

    Thus,

    bottomBoundaryRef.current = null

    at the end of the rendering cycle, becomes:

    bottomBoundaryRef.current = <div id="page-bottom-boundary" style="border: 1px solid red;"></div>

    We shall see where this assignment is done in a minute.

    Next, we define a scrollObserver function, in which to set the observer. This function accepts a DOM node to observe. The main point to note here is that whenever we hit the intersection under observation, we dispatch the ADVANCE_PAGE action. The effect is to increment the value of pager.page by 1. Once this happens, the useEffect hook that has it as a dependency is re-run. This re-run, in turn, invokes the fetch call with the new page number.

    The event procession looks like this.

    Hit intersection under observation → call ADVANCE_PAGE action → increment value of pager.page by 1 → useEffect hook for fetch call runs → fetch call is run → returned images are concatenated to the images array.

    We invoke scrollObserver in a useEffect hook so that the function will run only when any of the hook’s dependencies change. If we didn’t call the function inside a useEffect hook, the function would run on every page render.

    Recall that bottomBoundaryRef.current refers to

    . We check that its value is not null before passing it to scrollObserver. Otherwise, the IntersectionObserver constructor would return an error.

    Because we used scrollObserver in a useEffect hook, we have to wrap it in a useCallback hook to prevent un-ending component re-renders. You can learn more about useCallback in the React docs.

    Enter the below code after the

    div.

    // App.js
    <div id='image'>
    ...
    </div>
    {imgData.fetching && (
      <div className="text-center bg-secondary m-auto p-3">
        <p className="m-0 text-white">Getting images</p>
      </div>
    )}
    <div id='page-bottom-boundary' style={{ border: '1px solid red' }} ref={bottomBoundaryRef}></div>

    When the API call starts, we set fetching to true, and the text Getting images becomes visible. As soon as it finishes, we set fetching to false, and the text gets hidden. We could also trigger the API call before hitting the boundary exactly by setting a different threshold in the constructor options object. The red line at the end lets us see exactly when we hit the page boundary.

    The corresponding branch at this point is 02-infinite-scroll.

    We will now implement image lazy loading.

    Implementing Image Lazy Loading

    If you inspect the network tab as you scroll down, you’ll see that as soon as you hit the red line (the bottom boundary), the API call happens, and all the images start loading even when you haven’t gotten to viewing them. There are a variety of reasons why this might not be desirable behavior. We may want to save network calls until the user wants to see an image. In such a case, we could opt for loading the images lazily, i.e., we won’t load an image until it scrolls into view.

    Open up src/App.js. Just below the infinite scrolling functions, enter the following code.

    // App.js
    
    // lazy loads images with intersection observer
    // only swap out the image source if the new url exists
    const imagesRef = useRef(null);
    const imgObserver = useCallback(node => {
      const intObs = new IntersectionObserver(entries => {
        entries.forEach(en => {
          if (en.intersectionRatio > 0) {
            const currentImg = en.target;
            const newImgSrc = currentImg.dataset.src;
            // only swap out the image source if the new url exists
            if (!newImgSrc) {
              console.error('Image source is invalid');
            } else {
              currentImg.src = newImgSrc;
            }
            intObs.unobserve(node); // detach the observer when done
          }
        });
      })
      intObs.observe(node);
    }, []);
    useEffect(() => {
      imagesRef.current = document.querySelectorAll('.card-img-top');
      if (imagesRef.current) {
        imagesRef.current.forEach(img => imgObserver(img));
      }
    }, [imgObserver, imagesRef, imgData.images]);
    

    As with scrollObserver, we define a function, imgObserver, which accepts a node to observe. When the page hits an intersection, as determined by en.intersectionRatio > 0, we swap the image source on the element. Notice that we first check if the new image source exists before doing the swap. As with the scrollObserver function, we wrap imgObserver in a useCallback hook to prevent un-ending component re-render.

    Also note that we stop observer an img element once we’re done with the substitution. We do this with the unobserve method.

    In the following useEffect hook, we grab all the images with a class of .card-img-top on the page with document.querySelectorAll. Then we iterate over each image and set an observer on it.

    Note that we added imgData.images as a dependency of the useEffect hook. When this changes it triggers the useEffect hook and in turn imgObserver get called with each element.

    Update the element as shown below.

    <img
      alt={author}
      data-src={download_url}
      className="card-img-top"
      src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'}
    />

    We set a default source for every element and store the image we want to show on the data-src property. The default image usually has a small size so that we’re downloading as little as possible. When the element comes into view, the value on the data-src property replaces the default image.

    In the picture below, we see the default lighthouse image still showing in some of the spaces.

    Images being lazily loaded. (Large preview)

    The corresponding branch at this point is 03-lazy-loading.

    Let’s now see how we can abstract all these functions so that they’re re-usable.

    Abstracting Fetch, Infinite Scroll And Lazy Loading Into Custom Hooks

    We have successfully implemented fetch, infinite scroll, and image lazy loading. We might have another component in our application that needs similar functionality. In that case, we could abstract and reuse these functions. All we have to do is move them inside a separate file and import them where we need them. We want to turn them into Custom Hooks.

    The React documentation defines a Custom Hook as a JavaScript function whose name starts with "use" and that may call other hooks. In our case, we want to create three hooks, useFetch, useInfiniteScroll, useLazyLoading.

    Create a file inside the src/ folder. Name it customHooks.js and paste the code below inside.

    // customHooks.js
    
    import { useEffect, useCallback, useRef } from 'react';
    // make API calls and pass the returned data via dispatch
    export const useFetch = (data, dispatch) => {
      useEffect(() => {
        dispatch({ type: 'FETCHING_IMAGES', fetching: true });
        fetch(`https://picsum.photos/v2/list?page=${data.page}&limit=10`)
          .then(data => data.json())
          .then(images => {
            dispatch({ type: 'STACK_IMAGES', images });
            dispatch({ type: 'FETCHING_IMAGES', fetching: false });
          })
          .catch(e => {
            dispatch({ type: 'FETCHING_IMAGES', fetching: false });
            return e;
          })
      }, [dispatch, data.page])
    }
    
    // next code block here

    The useFetch hook accepts a dispatch function and a data object. The dispatch function passes the data from the API call to the App component, while the data object lets us update the API endpoint URL.

    // infinite scrolling with intersection observer
    export const useInfiniteScroll = (scrollRef, dispatch) => {
      const scrollObserver = useCallback(
        node => {
          new IntersectionObserver(entries => {
            entries.forEach(en => {
              if (en.intersectionRatio > 0) {
                dispatch({ type: 'ADVANCE_PAGE' });
              }
            });
          }).observe(node);
        },
        [dispatch]
      );
      useEffect(() => {
        if (scrollRef.current) {
          scrollObserver(scrollRef.current);
        }
      }, [scrollObserver, scrollRef]);
    }
    
    // next code block here

    The useInfiniteScroll hook accepts a scrollRef and a dispatch function. The scrollRef helps us set up the observer, as already discussed in the section where we implemented it. The dispatch function gives a way to trigger an action that updates the page number in the API endpoint URL.

    // lazy load images with intersection observer
    export const useLazyLoading = (imgSelector, items) => {
      const imgObserver = useCallback(node => {
      const intObs = new IntersectionObserver(entries => {
        entries.forEach(en => {
          if (en.intersectionRatio > 0) {
            const currentImg = en.target;
            const newImgSrc = currentImg.dataset.src;
            // only swap out the image source if the new url exists
            if (!newImgSrc) {
              console.error('Image source is invalid');
            } else {
              currentImg.src = newImgSrc;
            }
            intObs.unobserve(node); // detach the observer when done
          }
        });
      })
      intObs.observe(node);
      }, []);
      const imagesRef = useRef(null);
      useEffect(() => {
        imagesRef.current = document.querySelectorAll(imgSelector);
        if (imagesRef.current) {
          imagesRef.current.forEach(img => imgObserver(img));
        }
      }, [imgObserver, imagesRef, imgSelector, items])
    }
    

    The useLazyLoading hook receives a selector and an array. The selector is used to find the images. Any change in the array triggers the useEffect hook that sets up the observer on each image.

    We can see that it is the same functions we have in src/App.js that we have extracted to a new file. The good thing now is that we can pass arguments dynamically. Let’s now use these custom hooks in the App component.

    Open src/App.js. Import the custom hooks and delete the functions we defined for fetching data, infinite scroll, and image lazy loading. Leave the reducers and the sections where we make use of useReducer. Paste in the below code.

    // App.js
    
    // import custom hooks
    import { useFetch, useInfiniteScroll, useLazyLoading } from './customHooks'
    
      const imgReducer = (state, action) => { ... } // retain this
      const pageReducer = (state, action) => { ... } // retain this
      const [pager, pagerDispatch] = useReducer(pageReducer, { page: 0 }) // retain this
      const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true }) // retain this
    
    let bottomBoundaryRef = useRef(null);
    useFetch(pager, imgDispatch);
    useLazyLoading('.card-img-top', imgData.images)
    useInfiniteScroll(bottomBoundaryRef, pagerDispatch);
    
    // retain the return block
    return (
      ...
    )

    We have already talked about bottomBoundaryRef in the section on infinite scroll. We pass the pager object and the imgDispatch function to useFetch. useLazyLoading accepts the class name .card-img-top. Note the . included in the class name. By doing this, we don’t need to specify it document.querySelectorAll. useInfiniteScroll accepts both a ref and the dispatch function for incrementing the value of page.

    The corresponding branch at this point is 04-custom-hooks.

    Conclusion

    HTML is getting better at providing nice APIs for implementing cool features. In this post, we’ve seen how easy it is to use the intersection observer in a React functional component. In the process, we learned how to use some of React’s hooks and how to write our own hooks.

    Resources

    • “Infinite Scroll + Image Lazy Loading,” Orji Chidi Matthew, GitHub
    • “Infinite Scrolling, Pagination Or “Load More” Buttons? Usability Findings In eCommerce,” Christian Holst, Smashing Magazine
    • “Lorem Picsum,” David Marby & Nijiko Yonskai
    • “IntersectionObserver’s Coming Into View,” Surma, Web Fundamentals
    • Can I Use…IntersectionObserver
    • “Intersection Observer API,” MDN web docs
    • “Components And Props,” React
    • “useCallback,” React
    • “useReducer,” React
    Smashing Editorial
    (ks, ra, yk, il)

    From our sponsors: Implementing Infinite Scroll And Image Lazy Loading In React

    Posted on 16th March 2020Web Design
    FacebookshareTwittertweetGoogle+share

    Related posts

    Archived
    22nd March 2023
    Archived
    18th March 2023
    Archived
    20th January 2023
    Thumbnail for 25788
    Handling Continuous Integration And Delivery With GitHub Actions
    19th October 2020
    Thumbnail for 25778
    A Monthly Update With New Guides And Community Resources
    19th October 2020
    Thumbnail for 25781
    Supercharge Testing React Applications With Wallaby.js
    19th October 2020
    Latest News
    • Archived
      22nd March 2023
    • Archived
      18th March 2023
    • Archived
      20th January 2023
    • 20201019 ML Brief
      19th October 2020
    • Thumbnail for 25788
      Handling Continuous Integration And Delivery With GitHub Actions
      19th October 2020
    • Thumbnail for 25786
      The Future of CX with Larry Ellison
      19th October 2020
    News Categories
    • Digital Marketing
    • Web Design

    Our services

    Website Design
    Website Design

    A website is an important part of any business. Professional website development is an essential element of a successful online business.

    We provide website design services for every type of website imaginable. We supply brochure websites, E-commerce websites, bespoke website design, custom website development and a range of website applications. We love developing websites, come and talk to us about your project and we will tailor make a solution to match your requirements.

    You can contact us by phone, email or send us a request through our online form and we can give you a call back.

    More Information

    Digital Marketing
    Digital Marketing

    Our digital marketeers have years of experience in developing and excuting digital marketing strategies. We can help you promote your business online with the most effective methods to achieve the greatest return for your marketing budget. We offer a full service with includes the following:

    1. Social Media Marketing

    2. Email & Newsletter Advertising

    3. PPC - Pay Per Click

    4. A range of other methods are available

    More Information

    SEO
    SEO Services

    SEO is an essential part of owning an online property. The higher up the search engines that your website appears, the more visitors you will have and therefore the greater the potential for more business and increased profits.

    We offer a range of SEO services and packages. Our packages are very popular due to the expanse of on-page and off-page SEO services that they cover. Contact us to discuss your website and the SEO services that would best suit to increase your websites ranking.

    More Information

    E-commerce
    E-commerce Websites

    E-commerce is a rapidly growing area with sales online increasing year on year. A professional E-commerce store online is essential to increase sales and is a reflection of your business to potential customers. We provide professional E-commerce websites custom built to meet our clients requirements.

    Starting to sell online can be a daunting task and we are here to make that journey as smooth as possible. When you work with Cunningham Web Solutions on your E-commerce website, you will benefit from the experience of our team and every detail from the website design to stock management is carefully planned and designed with you in mind.

    More Information

    Social Media Services
    Social Media Services

    Social Media is becoming an increasingly effective method of marketing online. The opportunities that social media marketing can offer are endless and when managed correctly can bring great benefits to every business.

    Social Media Marketing is a low cost form of advertising that continues to bring a very good ROI for our clients. In conjuction with excellent website development and SEO, social media marketing should be an essential part of every digital marketing strategy.

    We offer Social Media Management packages and we also offer Social Media Training to individuals and to companies. Contact us to find out more.

    More Information

    Cunningham Web Solutions
    © Copyright 2025 | Cunningham Web Solutions
    • Home
    • Our Services
    • FAQ's
    • Account Services
    • Privacy Policy
    • Contact Us