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
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 asimport { useIntersection } from '@mantine/hooks';
The final code would look like this
I recommend 2 options for you issue:
https://www.npmjs.com/package/react-infinite-scroll-hook?fbclid=IwAR2MoMvq5qXnw5dEuCs1ea3YRa3mAtwv82NDTxA9u6XwfatfnbTl6qcvI5A.
https://www.npmjs.com/package/react-intersection-observer.
https://dev.to/producthackers/intersection-observer-using-react-49ko.