skip to Main Content

I have an array, it contains 1000 users. Then I run through this array using the map method and I get 1000 users per page. I need to make it so that initially it loads the first 50 users, and when the scroll reaches the end, it loads another 50 and so on until the number of users runs out. I tried to implement this in two ways, the first way didn’t work out for me (it only loaded the first 50 users and then nothing), the second way I tried to use the Intersection Observer, so at least some kind of scrolling appeared, but it still works with bugs (for example, 400 users and loads only 250).
To begin with, I will show the component without the implementation of lazy loading so that it is clear.
Component without lazy-loadning implementation:

import { useState, useEffect, useRef } from "react";
import { users } from "../../users/generateUsers.ts";
import UserItem from "../userItem/UserItem.tsx";
import { nanoid } from "nanoid";
import "../userList/UserList.css";

export default function UserList() {


  return (
    <div className="wrapper">
      <div className="content">
        {users.map((user, index) => (
          <div
            className={`userCard `}
            key={nanoid()}
      
          >
            <span
              className="card-number"
            >
              {index + 1}
            </span>
            <UserItem
              color={user.color}
              speed={user.speed}
              name={user.name}
              time={user.time}
              
            />
          </div>
        ))}
      </div>
     
    </div>
  );
}

The first version of lazy loading implementation:

import { useState, useEffect, useRef } from "react";
import { users } from "../../users/generateUsers.ts";
import UserItem from "../userItem/UserItem.tsx";
import { nanoid } from "nanoid";
import "../userList/UserList.css";
import { User } from "../../types/types.ts";


export default function UserList() {
  const batchSize = 50;
  const [loadedUsers, setLoadedUsers] = useState<User[]>([]);
  const [startIndex, setStartIndex] = useState(0);
  const [totalUsers, setTotalUsers] = useState(users.length);
  const [selectedUser, setSelectedUser] = useState<User | null>(null);
  const contentRef = useRef<HTMLDivElement>(null); // Reference to the content div

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

  const loadMoreUsers = () => {
    const endIndex = Math.min(startIndex + batchSize, totalUsers);
    const nextBatch = users.slice(startIndex, endIndex);

    setLoadedUsers((prevLoadedUsers) => [...prevLoadedUsers, ...nextBatch]);
    setStartIndex(endIndex);
  };

  const handleScroll = () => {
    const contentElement = contentRef.current;
    if (
      contentElement &&
      contentElement.scrollTop + contentElement.clientHeight ===
        contentElement.scrollHeight &&
      startIndex < totalUsers
    ) {
      loadMoreUsers();
    }
  };

  return (
    <div className="wrapper" onScroll={handleScroll}>
      <div className="content" ref={contentRef}>
        {loadedUsers.map((user, index) => (
          <div className={`userCard `} key={nanoid()}>
            <span className="card-number">{index + 1}</span>
            <UserItem
              color={user.color}
              speed={user.speed}
              name={user.name}
              time={user.time}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

Second version using Intersection Observer:

import { useState, useEffect, useRef } from "react";
import { users } from "../../users/generateUsers.ts";
import UserItem from "../userItem/UserItem.tsx";
import { nanoid } from "nanoid";
import "../userList/UserList.css";

export default function UserList() {
  const batchSize = 50;
  const [loadedUsers, setLoadedUsers] = useState<any[]>([]);
  const [startIndex, setStartIndex] = useState(0);
  const [totalUsers, setTotalUsers] = useState(users.length);
  const [selectedUser, setSelectedUser] = useState<any | null>(null);
  const sentinelRef = useRef<HTMLDivElement>(null);

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

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const target = entries[0];
        if (target.isIntersecting && startIndex < totalUsers) {
          loadMoreUsers();
        }
      },
      { threshold: 0.9 }
    );

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

    return () => {
      if (sentinelRef.current) {
        observer.unobserve(sentinelRef.current);
      }
    };
  }, [startIndex, totalUsers]);

  const loadMoreUsers = () => {
    const endIndex = Math.min(startIndex + batchSize, totalUsers);
    const nextBatch = users.slice(startIndex, endIndex);

    setLoadedUsers((prevLoadedUsers) => [...prevLoadedUsers, ...nextBatch]);
    setStartIndex(endIndex);
  };

  const handleUserClick = (user: any) => {
    setSelectedUser(user);
  };

  return (
    <div className="wrapper">
      <div className="content">
        {loadedUsers.map((user, index) => (
          <div
            className={`userCard `}
            key={nanoid()}
            onClick={() => handleUserClick(user)}
          >
            <span className="card-number">{index + 1}</span>
            <UserItem
              color={user.color}
              speed={user.speed}
              name={user.name}
              time={user.time}
              selected={selectedUser === user}
            />
          </div>
        ))}
      </div>
      <div ref={sentinelRef} style={{ height: "1px" }} />
    </div>
  );
}

2

Answers


  1. The best solution I can recommend is to use mantine hooks, you can install it from yarn add @mantine/hooks, then import the useIntersection hook as import { useIntersection } from '@mantine/hooks';

    The final code would look like this

    import { useIntersection } from '@mantine/hooks';
    import { useState, useEffect } from 'react';
    
    function InfinityList() {
          const [page, setPage] = useState(1);
          const [data, setData] = useState([]);
          const [myArray, setMyArray] = useState([1,2,3, ....1000])
    
          useEffect(() => {
            paginate();
          }, [page]);
    
          async function paginate() {
            // calculate start and end indexes for slicing data array
            const startIndex = (page - 1) * 50;
            const endIndex = page * 50;
    
            // slice data array to get next 50 items
            const newData = myArray.slice(startIndex, endIndex);
    
            // update state with new data
            setData((prevData) => [...prevData, ...newData]);
    
            // increment page number for next pagination
            setPage(page + 1);
          }
    
          // set up intersection observer
          const intersectionRef = useIntersection(paginate, {
            rootMargin: '20px',
            threshold: 0,
          });
    
          return (
            <div>
              {data.map((item) => (
                <div key={item.id}>your data here</div>
              ))}
    {/* the ref is used to listen to the intersection, when 20px of this is in view, it will get the other 50 items, and so on...*/}
              <div ref={intersectionRef} />
            </div>
          );
        }
    Login or Signup to reply.
  2. I recommend 2 options for you issue:

    1. use react-infinite-scroll-hook npm to scroll infinite scroll.
    import useInfiniteScroll from 'react-infinite-scroll-hook';
    
    function SimpleInfiniteList() {
      const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
    
      const [sentryRef] = useInfiniteScroll({
        loading,
        hasNextPage,
        onLoadMore: loadMore,
        // When there is an error, we stop infinite loading.
        // It can be reactivated by setting "error" state as undefined.
        disabled: !!error,
        // `rootMargin` is passed to `IntersectionObserver`.
        // We can use it to trigger 'onLoadMore' when the sentry comes near to become
        // visible, instead of becoming fully visible on the screen.
        rootMargin: '0px 0px 400px 0px',
      });
    
      return (
        <List>
          {items.map((item) => (
            <ListItem key={item.key}>{item.value}</ListItem>
          ))}
          {/* 
              As long as we have a "next page", we show "Loading" right under the list.
              When it becomes visible on the screen, or it comes near, it triggers 'onLoadMore'.
              This is our "sentry".
              We can also use another "sentry" which is separated from the "Loading" component like:
                <div ref={sentryRef} />
                {loading && <ListItem>Loading...</ListItem>}
              and leave "Loading" without this ref.
          */}
          {(loading || hasNextPage) && (
            <ListItem ref={sentryRef}>
              <Loading />
            </ListItem>
          )}
        </List>
      );
    }

    https://www.npmjs.com/package/react-infinite-scroll-hook?fbclid=IwAR2MoMvq5qXnw5dEuCs1ea3YRa3mAtwv82NDTxA9u6XwfatfnbTl6qcvI5A.

    import React from 'react';
    import { useInView } from 'react-intersection-observer';
    
    const Component = () => {
      const { ref, inView, entry } = useInView({
        /* Optional options */
        threshold: 0,
      });
    
      return (
        <div ref={ref}>
          <h2>{`Header inside viewport ${inView}.`}</h2>
        </div>
      );
    };
    1. use Intersection Observer.https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API.
      https://www.npmjs.com/package/react-intersection-observer.
      https://dev.to/producthackers/intersection-observer-using-react-49ko.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search