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
- Replace the mock with a real paginated API and append results.
Checks for Understanding
- What is the purpose of the sentinel element in an infinite list?
- How do you prevent multiple loads from triggering at once?
- When and how should the infinite loader stop requesting more items?
Answers
- It’s observed by IntersectionObserver; when it becomes visible, more items are loaded.
- Use a loading flag or internal guard and update hasMore only when new items are appended; debounce or rootMargin can also help.
- When you reach known total size or an API signals end of data; set hasMore=false to stop observing/loading.