javascript 64 lines · 10 steps

Infinite scroll with IntersectionObserver

A small class watches a sentinel element and fetches the next page whenever it scrolls into view.

Explained by highlit
1class InfiniteScroll {
2 constructor(sentinel, { loadMore, root = null, rootMargin = '200px' } = {}) {
3 this.sentinel = sentinel;
4 this.loadMore = loadMore;
5 this.page = 1;
6 this.loading = false;
7 this.done = false;
8 
9 this.observer = new IntersectionObserver(
10 (entries) => this.handleIntersect(entries),
11 { root, rootMargin, threshold: 0 }
12 );
13 this.observer.observe(this.sentinel);
14 }
15 
16 async handleIntersect(entries) {
17 const entry = entries[0];
18 if (!entry.isIntersecting || this.loading || this.done) return;
19 
20 this.loading = true;
21 this.sentinel.classList.add('loading');
22 
23 try {
24 const { items, hasMore } = await this.loadMore(this.page);
25 if (items.length) this.page += 1;
26 if (!hasMore) this.stop();
27 } catch (err) {
28 console.error('Failed to load more items', err);
29 } finally {
30 this.loading = false;
31 this.sentinel.classList.remove('loading');
32 }
33 }
34 
35 stop() {
36 this.done = true;
37 this.observer.unobserve(this.sentinel);
38 }
39 
40 disconnect() {
41 this.observer.disconnect();
42 }
43}
44 
45const list = document.querySelector('#feed');
46const sentinel = document.querySelector('#sentinel');
47 
48new InfiniteScroll(sentinel, {
49 async loadMore(page) {
50 const res = await fetch(`/api/posts?page=${page}`);
51 const { posts, hasMore } = await res.json();
52 
53 const fragment = document.createDocumentFragment();
54 for (const post of posts) {
55 const el = document.createElement('article');
56 el.className = 'post';
57 el.innerHTML = `<h2>${post.title}</h2><p>${post.excerpt}</p>`;
58 fragment.append(el);
59 }
60 list.append(fragment);
61 
62 return { items: posts, hasMore };
63 },
64});
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1IntersectionObserver turns scroll-position tracking into a cheap, callback-driven signal instead of a scroll listener.
  2. 2Guard flags like loading and done prevent overlapping fetches and stop work once the data runs out.
  3. 3A try/finally around the async fetch guarantees state and UI are reset even when a request fails.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Infinite scroll with IntersectionObserver — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code