Intersection Observer
Intersection Observer
Intersection Observer detects when elements enter or leave the viewport.
Basic Implementation
// hooks/useInfiniteScroll.js
function useInfiniteScroll(callback, options = {}) {
const { threshold = 0.1, rootMargin = '100px' } = options;
const observerRef = useRef(null);
const sentinelRef = useRef(null);
useEffect(() => {
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
callback();
}
},
{ threshold, rootMargin }
);
if (sentinelRef.current) {
observerRef.current.observe(sentinelRef.current);
}
return () => observerRef.current?.disconnect();
}, [callback, threshold, rootMargin]);
return sentinelRef;
}
With React Query
function InfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) =>
fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const sentinelRef = useInfiniteScroll(
() => fetchNextPage(),
{ rootMargin: '200px' }
);
const allPosts = data?.pages.flatMap(page => page.items) || [];
if (isLoading) return <Spinner />;
return (
<div className="infinite-list">
{allPosts.map(post => (
<article key={post.id} className="post-card">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
{hasNextPage && (
<div ref={sentinelRef} className="sentinel">
{isFetchingNextPage && <Spinner />}
</div>
)}
</div>
);
}
Custom Hook with Options
function useInfiniteScrollList({ queryKey, fetchFn, getNextCursor }) {
const [items, setItems] = useState([]);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const response = await fetchFn(cursor);
setItems(prev => [...prev, ...response.items]);
setCursor(getNextCursor(response));
setHasMore(response.hasMore);
} finally {
setIsLoading(false);
}
}, [cursor, hasMore, isLoading, fetchFn, getNextCursor]);
const sentinelRef = useInfiniteScroll(loadMore);
return { items, sentinelRef, isLoading, hasMore };
}
Scroll Restoration
function InfiniteListWithScrollRestore() {
const [scrollPosition, setScrollPosition] = useState(0);
const listRef = useRef(null);
// Save scroll position
useEffect(() => {
const handleScroll = () => {
sessionStorage.setItem('scrollPosition', window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Restore scroll position
useEffect(() => {
const savedPosition = sessionStorage.getItem('scrollPosition');
if (savedPosition) {
window.scrollTo(0, parseInt(savedPosition));
}
}, []);
// Rest of component...
}
Virtual Lists
Virtual Lists
Virtualization renders only visible items, enabling smooth scrolling of thousands of items.
Basic Virtual List
// components/VirtualList.jsx
function VirtualList({ items, itemHeight = 50, containerHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(startIndex, endIndex);
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
return (
<div
className="virtual-list"
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{ height: itemHeight }}
className="virtual-list-item"
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}
Using react-window
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-item">
<span>{items[index].name}</span>
</div>
);
return (
<FixedSizeList
height={400}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
Variable Size Lists
import { VariableSizeList } from 'react-window';
function VariableVirtualList({ items }) {
const listRef = useRef(null);
const getItemSize = (index) => {
const item = items[index];
return item.expanded ? 150 : 60;
};
const Row = ({ index, style }) => (
<div style={style} className="list-item">
<h4>{items[index].title}</h4>
{items[index].expanded && (
<p>{items[index].content}</p>
)}
</div>
);
return (
<VariableSizeList
ref={listRef}
height={400}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
Performance Considerations
Optimization Tips:
- Use fixed item heights when possible
- Avoid expensive computations in row rendering
- Use memo for row components
- Implement overscan (render extra items)
- Consider windowing for 1000+ items
Performance Considerations
Performance Considerations
Memory Management
// Clean up old items to prevent memory leaks
function useInfiniteScrollMemory(items, setItems, maxItems = 500) {
useEffect(() => {
if (items.length > maxItems) {
const itemsToRemove = items.length - maxItems;
setItems(prev => prev.slice(itemsToRemove));
}
}, [items.length, setItems, maxItems]);
}
Debounced Loading
function useDebouncedInfiniteScroll(callback, delay = 100) {
const timeoutRef = useRef(null);
const isLoadingRef = useRef(false);
const debouncedCallback = useCallback(() => {
if (isLoadingRef.current) return;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(async () => {
isLoadingRef.current = true;
await callback();
isLoadingRef.current = false;
}, delay);
}, [callback, delay]);
return debouncedCallback;
}
Pre-fetching
function usePrefetchOnScroll({ queryKey, fetchFn, threshold = 0.8 }) {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey,
queryFn: fetchFn,
});
useEffect(() => {
const handleScroll = () => {
const scrollPercentage =
window.scrollY / (document.body.scrollHeight - window.innerHeight);
if (scrollPercentage > threshold && hasNextPage) {
fetchNextPage();
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [threshold, hasNextPage, fetchNextPage]);
}
Error Recovery
function InfiniteListWithErrorRecovery() {
const [retryCount, setRetryCount] = useState(0);
const maxRetries = 3;
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isError,
} = useInfiniteQuery({
queryKey: ['items', retryCount],
queryFn: fetchItems,
getNextPageParam: (lastPage) => lastPage.nextCursor,
retry: false,
});
const handleRetry = () => {
if (retryCount < maxRetries) {
setRetryCount(prev => prev + 1);
}
};
if (isError && retryCount >= maxRetries) {
return (
<div>
<p>Failed to load items</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.items.map(item => (
<Item key={item.id} item={item} />
))}
</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
{isError && ' (Click to retry)'}
</button>
)}
</div>
);
}
Performance Checklist
- Implement virtualization for 1000+ items
- Use passive scroll listeners
- Debounce scroll handlers
- Clean up old items from memory
- Pre-fetch next page before reaching end
- Handle errors with retry logic
- Show loading indicators
Practice Problems
Create a reusable React component implementing Infinite Scroll. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Infinite Scroll using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Infinite Scroll for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What does Intersection Observer detect?
2. When should you use virtualization?
3. What is overscan in virtual lists?
4. Why use passive scroll listeners?
5. What should you do to prevent memory leaks in infinite scroll?
Flashcards
Question
What is Intersection Observer?
Click to reveal answer
Answer
An API that detects when elements enter or leave the viewport, used for infinite scroll triggers.
Question
What is virtualization?
Click to reveal answer
Answer
Rendering only visible items in a list to reduce DOM nodes and improve performance.
Question
What is overscan?
Click to reveal answer
Answer
Extra items rendered outside the viewport for smoother scrolling experience.
Question
Why use passive scroll listeners?
Click to reveal answer
Answer
They don't block scrolling, providing better performance for scroll event handling.
Question
What is Infinite Scroll?
Click to reveal answer
Answer
Infinite Scroll is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Intersection Observer is the modern way to detect scroll position
- 2.Virtualization is essential for lists with thousands of items
- 3.Always use passive scroll listeners for better performance
- 4.Pre-fetch next pages before users reach the end
- 5.Clean up old items to prevent memory leaks
Interview Tips
- •Explain how Intersection Observer works and its use cases
- •Discuss when virtualization is necessary
- •Know the performance implications of infinite scroll
Cheat Sheet
Infinite Scroll Cheat Sheet
Intersection Observer
const observer = new IntersectionObserver(callback, {
threshold: 0.1,
rootMargin: '100px'
});
observer.observe(sentinel);
Virtual List
- Render only visible items
- Use react-window or react-virtualized
- Set fixed item heights for best performance
- Add overscan for smooth scrolling
Performance Tips
- Use passive scroll listeners
- Debounce scroll handlers
- Pre-fetch next page
- Clean up old items from memory