Native Lazy Loading with Intersection Observer in React
Cover photo by Sergey Semenov on Unsplash
Infinite scrolling or "lazy loading" lists is a common pattern in modern web applications. While there are many libraries available to handle this (like react-intersection-observer), implementing it natively using the Intersection Observer API is surprisingly straightforward, lightweight, and performant.
In this tutorial, we'll build a simple infinite scroll list in React without installing any extra packages.
#What is Intersection Observer?
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
Simply put, it triggers a callback function whenever an element you're watching enters or exits the screen.
#Browser Support
Good news! As of 2026, the Intersection Observer API has excellent browser support. It is available in all modern browsers:
- Chrome: 51+
- Edge: 15+
- Firefox: 55+
- Safari: 12.1+ (iOS 12.2+)
- Opera: 38+
It is not supported in Internet Explorer. If you need to support IE11, you will need to use a polyfill.
#The Implementation
We will create a list that fetches more items when the user scrolls to the bottom. We'll place a transparent "sentinel" element at the end of the list. When this sentinel becomes visible, we trigger our load function.
#1. Setting up the State
First, let's set up our component with some state to hold our items and a loading status.
import { useState, useRef, useEffect, useCallback } from "react"
export default function InfiniteList() {
const [items, setItems] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
// Ref for the element we want to observe
const observerTarget = useRef<HTMLDivElement>(null)
// ... implementation continues
}#2. Fetching Data
Let's create a function to simulate fetching data. In a real app, this would be an API call.
const fetchData = useCallback(async () => {
setLoading(true)
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1000))
// Generate dummy items
const newItems = Array.from({ length: 10 }, (_, i) => `Item ${items.length + i + 1}`)
setItems((prev) => [...prev, ...newItems])
setPage((prev) => prev + 1)
setLoading(false)
}, [items.length])#3. Implementing the Observer
Now for the magic. We use useEffect to attach the observer to our target element.
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
// entries[0] is our target element
if (entries[0].isIntersecting && !loading) {
fetchData()
}
},
{ threshold: 1.0 } // Trigger when 100% of the target is visible
)
const currentTarget = observerTarget.current
if (currentTarget) {
observer.observe(currentTarget)
}
// Cleanup on unmount
return () => {
if (currentTarget) {
observer.unobserve(currentTarget)
}
}
}, [fetchData, loading])#4. Putting It All Together
Finally, render the list and the sentinel element at the bottom.
return (
<div className="max-w-md mx-auto p-4 space-y-4">
<h1 className="text-2xl font-bold mb-4">Native Infinite Scroll</h1>
<div className="space-y-2">
{items.map((item, index) => (
<div
key={index}
className="p-4 bg-white dark:bg-neutral-800 rounded shadow border border-neutral-200 dark:border-neutral-700"
>
{item}
</div>
))}
</div>
{/* The Sentinel Element */}
<div ref={observerTarget} className="h-4 w-full flex justify-center p-4">
{loading && (
<span className="animate-spin h-5 w-5 border-2 border-blue-500 rounded-full border-t-transparent"></span>
)}
</div>
{!loading && items.length > 50 && (
<p className="text-center text-gray-500">You've reached the end!</p>
)}
</div>
)#Why use Native implementation?
- Bundle Size: You save the overhead of importing an external library.
- Control: You have full control over the
rootMarginandthresholdoptions. - Performance: The API is native to the browser and highly optimized, running off the main thread where possible.
#Conclusion
Using IntersectionObserver directly is a great way to handle lazy loading image, infinite scrolling lists, or triggering animations on scroll. It keeps your codebase lean and relies on standardized web platform features.