Why I Hate Infinite Scroll
TL;DR: Infinite scroll looks smooth but it's actually a UX nightmare. Here's why it sucks and what you should use instead (with code examples).
So I was building this feed feature and thought "infinite scroll, that's smooth UX right?" Wrong. So damn wrong.
After implementing it and watching users struggle with it, I realized infinite scroll is one of those things that sounds good in theory but falls apart in practice. Let me tell you why.
Why Infinite Scroll Actually Sucks
I'll be honest - infinite scroll feels cool when you first implement it. But then you watch real users and realize you've created a UX nightmare. Let me break down each problem with real examples.
Your Users Get Lost in the Void
Picture this: You're building an e-commerce site. User scrolls through 50 products, finds a cool jacket, keeps scrolling, finds some shoes, then wants to go back to that jacket. Good luck! There's no page 3 or page 5 reference. They're just... somewhere in the endless void.
I watched a user in testing scroll up and down for 2 minutes looking for a product they saw earlier. They gave up and left the site. That's a lost sale because of "smooth UX."
Here's what happens in your code:
// Your user's mental model: "It was around item 47" // Your code's reality: "Here's items 1-200 in one giant list" function ProductList() { const [products, setProducts] = useState([]); // User has no idea where they are in this endless array useEffect(() => { fetchMoreProducts().then(newProducts => setProducts(prev => [...prev, ...newProducts]) // Just keeps growing ); }, []); return ( <div> {products.map((product, index) => ( // User sees item but has no reference point <ProductCard key={product.id} product={product} /> ))} </div> ); }
RIP Your Footer (and Everything Important)
This one drives me crazy. You spend time crafting a beautiful footer with important links, contact info, legal stuff, newsletter signup. Then you implement infinite scroll and BAM - nobody will ever see it again.
Real example: I worked on a site where the client complained about low newsletter signups. The signup form was in the footer. With infinite scroll, the footer literally doesn't exist anymore. It's like having a store where the checkout counter keeps moving further away every time someone walks toward it.
function App() { return ( <div> <Header /> <InfiniteScrollList /> {/* This footer will NEVER be seen by users */} <Footer> <NewsletterSignup /> <ContactInfo /> <LegalLinks /> <SocialMedia /> </Footer> </div> ); }
The footer becomes digital purgatory. Users can't reach it, search engines can't properly index it, and your conversion rates tank.
Performance Goes to Hell
Here's where it gets really bad. Your DOM just keeps growing. And growing. And growing. I've seen infinite scroll implementations with 10,000+ DOM nodes. The browser starts crying.
Watch this performance nightmare unfold:
function InfiniteList() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const loadMore = async () => { setLoading(true); const newItems = await fetchItems(); // This is the problem - we never remove anything setItems(prev => [...prev, ...newItems]); setLoading(false); }; // After 10 scroll sessions, you have 1000+ DOM nodes // Browser: "Please... stop..." return ( <div> {items.map(item => ( <HeavyComponent key={item.id} item={item} /> ))} {loading && <div>Loading...</div>} </div> ); }
I measured one infinite scroll implementation: after 5 minutes of scrolling, the page was using 500MB of memory and scrolling had a 200ms delay. On mobile, it was basically unusable.
Screen Readers Hate You (And Your Users)
Accessibility isn't just nice-to-have, it's the law in many places. But infinite scroll breaks screen readers in spectacular ways.
Here's what happens: Screen reader user navigates to your content list. They start reading. Suddenly, new content appears and their focus gets lost. They have to start over. New content appears again. They start over again. It's like trying to read a book where someone keeps adding pages in the middle while you're reading.
function AccessibilityNightmare() { const [items, setItems] = useState([]); useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { // This completely breaks screen reader navigation fetchMoreItems().then(newItems => { setItems(prev => [...prev, ...newItems]); // Screen reader: "Where am I? What happened to my focus?" }); } }); // ... observer setup }, []); return ( <div> {items.map(item => ( // Screen reader users lose their place when new items inject <div key={item.id} tabIndex={0}> {item.content} </div> ))} </div> ); }
I tested this with VoiceOver on Mac. The experience was so frustrating I wanted to throw my laptop out the window.
No Bookmarks = No Sharing = No Growth
Users can't bookmark a specific position. They can't share "hey check out this product on page 3." Everything is just one giant URL.
Real scenario: User finds a perfect product, shares the link with their friend. Friend clicks link, sees the first page of products, has no idea what their friend was talking about. Friend gives up.
// User's mental model: "I want to share this specific item" // Your URL: "https://yoursite.com/products" (useless) // What they need: "https://yoursite.com/products?page=3&item=47" function ShareableContent() { const [products, setProducts] = useState([]); // URL never changes, user can't deep-link to specific content return ( <div> {products.map(product => ( <div key={product.id}> {product.name} {/* This share button is basically useless */} <ShareButton url={window.location.href} /> </div> ))} </div> ); }
Analytics Become Meaningless
How do you measure engagement when there are no pages? How do you know if users are actually finding what they want or just endlessly scrolling because they're lost?
Traditional analytics rely on page views, bounce rates, time on page. Infinite scroll breaks all of that:
// Traditional analytics gtag('config', 'GA_TRACKING_ID', { page_title: 'Products - Page 3', page_location: 'https://site.com/products?page=3' }); // Infinite scroll analytics gtag('config', 'GA_TRACKING_ID', { page_title: 'Products - ¯\_(ツ)_/¯', page_location: 'https://site.com/products' // Same URL forever });
I worked with a client who couldn't figure out why their conversion rates were so low. Turns out, their analytics showed everyone was "engaged" (long time on page) but really users were just lost and scrolling around confused.
The Memory Leak Problem
This is the technical nightmare that haunts your 3 AM debugging sessions. Event listeners pile up, components don't unmount properly, and memory usage climbs until your app crashes:
function MemoryLeakHell() { const [items, setItems] = useState([]); useEffect(() => { const handleScroll = () => { // This function gets called thousands of times if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { loadMoreItems(); } }; window.addEventListener('scroll', handleScroll); // If you forget this cleanup, you're screwed return () => window.removeEventListener('scroll', handleScroll); }, []); const loadMoreItems = () => { // Each item might have its own event listeners // They all pile up in memory setItems(prev => [...prev, ...generateItems()]); }; return ( <div> {items.map(item => ( <ExpensiveComponent key={item.id} item={item} // Each component might have timers, listeners, etc. // All staying in memory forever /> ))} </div> ); }
I've debugged production apps where infinite scroll caused memory leaks that crashed the browser tab after 10 minutes of use.
Yeah, I learned all of this the hard way. Each problem individually is annoying, but together they create a user experience that looks smooth on the surface but is actually broken underneath.
OK But What About The Good Parts?
Look, I get it. Infinite scroll isn't completely useless. Here's when it actually makes sense:
Use it for:
- Social media feeds where users just want to mindlessly scroll
- Image galleries where you're just browsing casually
- News feeds where you don't need to reference specific items
Don't use it for:
- E-commerce (users need to compare, bookmark, filter)
- Search results (people want to go back and forth)
- Any content with important footer info
- Documentation or blogs where users need to navigate
The Lazy Implementation (Don't Do This)
Here's what most people do - the naive approach that causes all the problems:
import { useEffect, useRef } from 'react'; function InfiniteList({ items, fetchNext }) { const loaderRef = useRef(null); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => entry.isIntersecting && fetchNext(), { rootMargin: '200px' } ); if (loaderRef.current) observer.observe(loaderRef.current); return () => observer.disconnect(); }, [fetchNext]); return ( <ul> {items.map(item => <li key={item.id}>{item.title}</li>)} <li ref={loaderRef}>Loading more...</li> </ul> ); }
This looks clean but it's a trap. Your DOM will keep growing, performance will tank, and users will hate you.
Better Alternatives (Actually Good UX)
Instead of infinite scroll, use these patterns that don't suck:
1. "Load More" Button - The Simple Win
Just give users control. They click when they want more content:
import { useState, useEffect } from 'react'; function LoadMoreList() { const [items, setItems] = useState([]); const [page, setPage] = useState(1); useEffect(() => { fetch(`/api/items?page=${page}`) .then(res => res.json()) .then(data => setItems(prev => [...prev, ...data])); }, [page]); return ( <> <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul> <button onClick={() => setPage(p => p + 1)}>Load More</button> </> ); }
Users stay in control, DOM stays manageable, footer stays accessible. Win-win-win.
2. Virtualized Lists - For When You Have Tons of Data
Got thousands of items? Don't render them all at once - that's insane. Use virtualization:
npm install react-window
import { FixedSizeList as List } from 'react-window'; function VirtualList({ items }) { return ( <List height={600} itemCount={items.length} itemSize={50} width="100%" > {({ index, style }) => ( <div style={style}>{items[index].title}</div> )} </List> ); }
This is magic - you can have 10,000 items and only render what's visible. Performance stays smooth, users stay happy.
Libraries to try: react-window
, react-virtualized
, react-virtuoso
.
3. Good Old Pagination - Still Works Great
Yeah it's not trendy, but pagination is solid:
// Next.js example export async function getServerSideProps({ query }) { const page = query.page || 1; const res = await fetch(`https://api.example.com/items?page=${page}`); return { props: { items: await res.json(), page } }; }
SEO-friendly, shareable URLs, users know where they are. Sometimes boring solutions are the best ones.
4. Let Users Filter Instead of Scroll
Instead of making users scroll through everything, let them find what they want:
function FilterableList({ items }) { const [q, setQ] = useState(''); const filtered = items.filter(i => i.title.includes(q)); return ( <> <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search..." /> <ul>{filtered.map(i => <li key={i.id}>{i.title}</li>)}</ul> </> ); }
Users get to the content they want without endless scrolling. Much better UX.
Tools That Don't Suck
If you absolutely must do infinite scroll:
react-infinite-scroll-component
(least bad option)@tanstack/react-query
withuseInfiniteQuery
- Use
IntersectionObserver
instead of scroll events throttle
/debounce
from Lodash for scroll handling
But seriously, try the alternatives first.
The Bottom Line
Infinite scroll is like that flashy feature that looks good in demos but creates real problems for real users. Your users want to find content, not get lost in an endless void.
Save yourself the headache and use patterns that actually work. Your users (and your performance metrics) will thank you.
Happy coding! 🚀