React - Infinite Scroll

Overview

Estimated time: 15–25 minutes

Implement infinite scrolling with a sentinel element that loads more items when visible.

Try it: Infinite list

View source
function App(){
  const [items, setItems] = React.useState(() => Array.from({length:20}, (_,i)=> `Row ${i+1}`));
  const [hasMore, setHasMore] = React.useState(true);
  const sentinelRef = React.useRef(null);
  React.useEffect(() => {
    const el = sentinelRef.current; if (!el) return;
    const io = new IntersectionObserver(async ([e]) => {
      if (e.isIntersecting && hasMore){
        await new Promise(r => setTimeout(r, 400));
        setItems(arr => {
          const next = arr.length + 10;
          const added = Array.from({length:10}, (_,i)=> `Row ${arr.length + i + 1}`);
          if (next >= 100) setHasMore(false);
          return [...arr, ...added];
        });
      }
    }, {rootMargin:'200px'});
    io.observe(el); return () => io.disconnect();
  }, [hasMore]);
  return (
    <div style={{height:320, overflow:'auto', border:'1px solid var(--border)', padding:8}}>
      <ul>{items.map(x => <li key={x}>{x}</li>)}</ul>
      <div ref={sentinelRef} aria-hidden="true" style={{height:1}} />
      <div aria-live="polite">{hasMore? 'Loading more…' : 'No more items'}</div>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('try-infinite')).render(<App />);

Syntax primer

  • Place a sentinel element at the end of the list; load when it intersects.

Common pitfalls

  • Multiple simultaneous loads—guard with a loading flag.

Exercises

  1. Replace the mock with a real paginated API and append results.

Checks for Understanding

  1. What is the purpose of the sentinel element in an infinite list?
  2. How do you prevent multiple loads from triggering at once?
  3. When and how should the infinite loader stop requesting more items?
Answers
  1. It’s observed by IntersectionObserver; when it becomes visible, more items are loaded.
  2. Use a loading flag or internal guard and update hasMore only when new items are appended; debounce or rootMargin can also help.
  3. When you reach known total size or an API signals end of data; set hasMore=false to stop observing/loading.