<div id="root"></div>
/* The entire styling is totally optional */
@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap");
* {
font-family: Roboto;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.item {
width: 80vw;
margin: 12px 0;
padding: 32px;
border-radius: 8px;
font-size: larger;
}
.loader {
border: 8px solid #cccccc;
border-top: 8px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 0.6s linear infinite;
display: inline-block;
margin: 8px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
const MAX_PAGES = 5;
const generateRandomColor = () => {
const characters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += characters[Math.floor(Math.random() * 16)];
}
return color;
};
const Item = ({ children, color, reference }) => {
return (
<div className="item" style={{ backgroundColor: color }} ref={reference}>
{children}
</div>
);
};
const App = () => {
const [items, setItems] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(false);
const [hasMore, setHasMore] = React.useState(true);
const [pages, setPages] = React.useState(0);
const observer = React.useRef();
React.useEffect(() => {
updateItems();
setPages((pages) => pages + 1);
}, []);
const lastItemRef = React.useCallback(
(node) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
if (pages < MAX_PAGES) {
updateItems();
setPages((pages) => pages + 1);
} else {
setHasMore(false);
}
}
});
if (node) observer.current.observe(node);
},
[isLoading, hasMore]
);
const updateItems = async () => {
setIsLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setItems((currItems) => {
const lastItem = currItems.length;
const updatedItems = [...currItems];
for (let i = 1; i <= 5; i++) {
const item = {
count: lastItem + i,
color: generateRandomColor()
};
updatedItems.push(item);
}
return updatedItems;
});
setIsLoading(false);
};
return (
<React.Fragment>
<h1>Infinite Scroll Demo</h1>
{items.map((item, index) =>
index + 1 === items.length ? (
<Item reference={lastItemRef} key={index} color={item.color}>
{item.count}
</Item>
) : (
<Item key={index} color={item.color}>
{item.count}
</Item>
)
)}
{isLoading && <div className="loader" />}
</React.Fragment>
);
};
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
View Compiled
This Pen doesn't use any external CSS resources.