Rendering large lists in a React application can quickly become a performance bottleneck. Imagine a list of thousands of items—rendering all of them at once can overwhelm the browser, slow down the app, and lead to poor user experience. Enter virtualized scrolling, a technique that ensures only visible items are rendered at any given time.
In this article, we'll dive into the problem of rendering large lists, explore a solution using a custom React hook called useVirtualizedList
, and provide a detailed implementation guide.
The Problem: Rendering Large Lists
When rendering a large dataset, React creates DOM nodes for every item in the list. This approach has several drawbacks:
- High Memory Usage: Every rendered DOM node consumes memory. For large lists, this can lead to significant memory overhead.
- Poor Rendering Performance: Rendering thousands of items at once takes time, leading to a sluggish UI.
- Reflows and Repaints: Browsers struggle to manage the layout and painting of large DOM trees, impacting smooth scrolling.
Example Scenario
Imagine you're building an app that displays a catalog of 10,000 products. Each product has a height of 30 pixels, and the container can only show 10 products at a time. Rendering all 10,000 products means unnecessarily creating 10,000 DOM nodes—even though only 10 are visible!
The Solution: Virtualized Scrolling
Virtualized scrolling dynamically calculates which items are visible within the viewport and renders only those items. The technique ensures that as users scroll, the rendered DOM updates to show only the necessary items while maintaining smooth performance.
Introducing useVirtualizedList
Our custom hook, useVirtualizedList
, simplifies virtualized scrolling. It accepts a list of items, the height of each item, and the height of the container. The hook returns the visible items, offsets for positioning, and a scroll handler to manage updates.
Implementation: Building the useVirtualizedList
Hook
Here's how the hook is implemented:
Hook Code
import { useEffect, useState } from "react";
type VirtualizedListProps<T> = {
items: T[];
itemHeight: number;
containerHeight: number;
};
export const useVirtualizedList = <T>({
items,
itemHeight,
containerHeight,
}: VirtualizedListProps<T>) => {
const [startIndex, setStartIndex] = useState(0);
const [endIndex, setEndIndex] = useState(0);
const totalVisibleItems = Math.ceil(containerHeight / itemHeight);
useEffect(() => {
setEndIndex(totalVisibleItems);
}, [containerHeight, itemHeight, totalVisibleItems]);
const handleScroll = (scrollTop: number) => {
const newStartIndex = Math.floor(scrollTop / itemHeight);
setStartIndex(newStartIndex);
setEndIndex(newStartIndex + totalVisibleItems);
};
const visibleItems = items.slice(startIndex, endIndex);
const offsetTop = startIndex * itemHeight;
const offsetBottom =
(items.length - visibleItems.length) * itemHeight - offsetTop;
return { visibleItems, offsetTop, offsetBottom, handleScroll };
};
How It Works
-
State Management:
-
startIndex
andendIndex
determine which items from the list should be visible. - These indices are dynamically updated based on the scroll position.
-
-
Initial Setup:
- The
useEffect
initializes theendIndex
based on the container's height and item height.
- The
-
Scroll Handler:
- The
handleScroll
function calculates the newstartIndex
based on the scroll position (scrollTop
) and updates thestartIndex
andendIndex
accordingly.
- The
-
Offsets:
-
offsetTop
: The total height of items above the visible area. -
offsetBottom
: The total height of items below the visible area.
-
Using the Hook in a Component
Here’s an example of how to integrate useVirtualizedList
into a component:
import React from "react";
import { useVirtualizedList } from "./useVirtualizedList";
const VirtualizedList = ({ items }: { items: string[] }) => {
const itemHeight = 50;
const containerHeight = 200;
const { visibleItems, offsetTop, offsetBottom, handleScroll } =
useVirtualizedList({
items,
itemHeight,
containerHeight,
});
return (
<div
style={{
height: containerHeight,
overflowY: "scroll",
position: "relative",
}}
onScroll={(e) => handleScroll(e.currentTarget.scrollTop)}
>
<div style={{ height: offsetTop }} />
{visibleItems.map((item, index) => (
<div
key={index}
style={{
height: itemHeight,
border: "1px solid lightgray",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{item}
</div>
))}
<div style={{ height: offsetBottom }} />
</div>
);
};
export default VirtualizedList;
Key Features in the Example
-
Dynamic Heights:
- The
offsetTop
andoffsetBottom
divs ensure the list scrolls correctly without rendering all items.
- The
-
Smooth Scroll Handling:
- The
onScroll
event updates the visible items based on the current scroll position.
- The
-
Seamless User Experience:
- Only visible items are rendered, improving performance for large datasets.
Testing the Hook
Testing hooks is crucial for ensuring they handle edge cases. Here’s an example test using Vitest:
import { renderHook, act } from "@testing-library/react";
import { useVirtualizedList } from "../useVirtualizedList";
describe("useVirtualizedList", () => {
const items = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
const itemHeight = 30;
const containerHeight = 120;
it("should initialize with correct visible items and offsets", () => {
const { result } = renderHook(() =>
useVirtualizedList({ items, itemHeight, containerHeight })
);
expect(result.current.visibleItems).toEqual(["Item 0", "Item 1", "Item 2", "Item 3"]);
expect(result.current.offsetTop).toBe(0);
expect(result.current.offsetBottom).toBe(2880); // Remaining height
});
it("should update visible items on scroll", () => {
const { result } = renderHook(() =>
useVirtualizedList({ items, itemHeight, containerHeight })
);
act(() => {
result.current.handleScroll(60); // Scroll down
});
expect(result.current.visibleItems).toEqual(["Item 2", "Item 3", "Item 4", "Item 5"]);
expect(result.current.offsetTop).toBe(60);
});
});
Key Takeaways
- Virtualized scrolling is essential for performance when rendering large lists.
- The
useVirtualizedList
hook simplifies the implementation of virtualized scrolling in React. - Proper testing ensures the hook handles various scenarios like scrolling, empty lists, and dynamic updates.
By adopting this technique, you can enhance the performance and user experience of your React applications when working with large datasets.
Top comments (0)