Modern Web APIs
Browser APIs for lazy loading, responsive components, offline support, and more
Browser APIs unlock features once impossible or library-dependent. Today most ship natively.
What you'll learn
Use IntersectionObserver for lazy loading and infinite scroll
Track element resizes with ResizeObserver
Observe DOM mutations with MutationObserver
Access user location with Geolocation API
Persist data with Web Storage API
Run heavy computation off the main thread with Web Workers
Register Service Workers for offline support
IntersectionObserver
Detect when elements enter/exit the viewport. No scroll event listeners needed.
import { useEffect, useRef, useState } from "react"
function useInView(threshold = 0.5) {
const ref = useRef<HTMLDivElement>(null!)
const [inView, setInView] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => setInView(entry.isIntersecting),
{ threshold }
)
observer.observe(el)
return () => observer.disconnect()
}, [threshold])
return { ref, inView }
}
Lazy Image Loading
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { ref, inView } = useInView(0.1)
const [loaded, setLoaded] = useState(false)
return (
<div ref={ref} style={{ minHeight: 300, background: "#eee" }}>
{inView && (
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{ opacity: loaded ? 1 : 0, transition: "opacity 0.3s" }}
/>
)}
</div>
)
}
Infinite Scroll
function InfiniteScroll({ loadMore }: { loadMore: () => Promise<void> }) {
const { ref, inView } = useInView(0.1)
useEffect(() => {
if (inView) loadMore()
}, [inView, loadMore])
return <div ref={ref}>Loading more...</div>
}
// loadMore fires whenever the sentinel enters viewport
// No scroll event, no throttle, no math
Gotcha: threshold value
threshold: 1.0 means 100% visible. For infinite scroll, 0.1 or even 0.0 is better — fires as soon as the sentinel enters the viewport, triggering the next load before user reaches bottom.
ResizeObserver
React to element size changes. Essential for responsive components and charts.
import { useEffect, useRef, useState } from "react"
function useElementSize() {
const ref = useRef<HTMLDivElement>(null!)
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect
setSize({ width, height })
})
observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return { ref, ...size }
}
function ResponsiveContainer() {
const { ref, width, height } = useElementSize()
return (
<div ref={ref} style={{ resize: "both", overflow: "auto" }}>
<p>
Container: {width.toFixed(0)} x {height.toFixed(0)}px
</p>
</div>
)
}
MutationObserver
Watch DOM changes — useful for detecting third-party script injections.
function useMutationLogger(target: HTMLElement | null) {
useEffect(() => {
if (!target) return
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
console.log("Nodes added/removed:", mutation.addedNodes.length)
}
if (mutation.type === "attributes") {
console.log("Attribute changed:", mutation.attributeName)
}
})
})
observer.observe(target, {
childList: true,
attributes: true,
subtree: true,
})
return () => observer.disconnect()
}, [target])
}
Geolocation API
function useGeolocation() {
const [position, setPosition] = useState<GeolocationPosition | null>(null)
const [error, setError] = useState<string | null>(null)
const requestLocation = useCallback(() => {
if (!navigator.geolocation) {
setError("Geolocation not supported")
return
}
navigator.geolocation.getCurrentPosition(
(pos) => setPosition(pos),
(err) => setError(err.message),
{ enableHighAccuracy: true, timeout: 5000 }
)
}, [])
return { position, error, requestLocation }
}
Web Workers
Offload CPU-heavy work from the main thread:
// worker.ts — runs in separate thread
self.onmessage = (event: MessageEvent<number[]>) => {
const numbers = event.data
// Heavy computation — doesn't block UI
const sorted = numbers.sort((a, b) => a - b)
self.postMessage(sorted)
}
// Component — communicates with worker
function useSortWorker() {
const workerRef = useRef<Worker | null>(null)
const [result, setResult] = useState<number[] | null>(null)
useEffect(() => {
workerRef.current = new Worker(new URL("./worker.ts", import.meta.url))
workerRef.current.onmessage = (e) => setResult(e.data)
return () => workerRef.current?.terminate()
}, [])
const sort = (numbers: number[]) => {
workerRef.current?.postMessage(numbers)
}
return { result, sort }
}
Service Workers (Offline)
// sw.ts — intercepts fetch, serves from cache
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open("v1")
.then((cache) =>
cache.addAll(["/", "/index.html", "/styles.css", "/app.js"])
)
)
})
self.addEventListener("activate", (event) => {
// Clean old caches
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((k) => k !== "v1").map((k) => caches.delete(k)))
)
)
})
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then(
(cached) =>
cached ??
fetch(event.request).then((response) => {
// Cache new requests
const clone = response.clone()
caches.open("v1").then((cache) => cache.put(event.request, clone))
return response
})
)
)
})
Challenge
Build an infinite scroll list using IntersectionObserver.
Infinite Scroll with IntersectionObserver
Code
tsx
1import { useState, useEffect, useRef, useCallback } from "react"2 3type Item = { id: number; title: string }4 5async function fetchItems(page: number): Promise<Item[]> {6// Simulates API call7await new Promise((r) => setTimeout(r, 800))8return Array.from({ length: 10 }, (_, i) => ({9id: page _ 10 + i,10title: `Item ${page _ 10 + i}`,11}))12}13 14// Step 1: Create a ref for the sentinel element15// Step 2: Set up IntersectionObserver on the sentinel16// Step 3: When sentinel is visible (inView), fetch next page17// Step 4: Append fetched items to list18// Step 5: Show loading indicator while fetching19 20export default function InfiniteList() {21// Your code here22return (23 <div>24 <ul>{/* Mapped items */}</ul>25 <div ref={/* sentinel ref */}>26 {/* Loading indicator */}27 </div>28 </div>29)30}Key Takeaways
IntersectionObserver: lazy load images, infinite scroll — no scroll listeners
ResizeObserver: respond to individual element size changes
MutationObserver: detect DOM changes from scripts or user interaction
Geolocation API: getCurrentPosition with permission prompt
Web Workers: offload heavy CPU work off the main thread
Service Workers: intercept fetch, cache assets for offline support
Web Storage: localStorage (persistent) vs sessionStorage (tab-scoped)
All these APIs are native — no libraries required for basic usage