Rendering large lists in React applications can quickly become a performance bottleneck. Imagine a list with thousands of items – rendering them all at once could overwhelm the browser, slow down the application, and lead to a poor user experience. Enter virtual scrollinga technique that ensures that only visible items are rendered at any given time.
In this article, we will delve into the problem of rendering large lists and explore a solution using a custom React hook called useVirtualizedList
and provide detailed implementation guidance.
Problem: Rendering a large list
When rendering a large dataset, React creates DOM nodes for each item in the manifest. This approach has several disadvantages:
- High memory usage: Each rendered DOM node consumes memory. For large lists, this can cause significant memory overhead.
- Poor rendering performance: Rendering thousands of items simultaneously takes time, resulting in a slow UI.
- Reflow and recoat: Browsers have difficulty managing the layout and drawing of large DOM trees, which affects smooth scrolling.
Example scenario
Suppose you are building an application that displays a catalog of 10,000 products. Each product is 30 pixels tall, and the container can only display 10 products at a time. Rendering all 10,000 products means creating 10,000 DOM nodes unnecessarily – even though only 10 are visible!
Solution: Virtual Scroll
Virtualized scrolling dynamically calculates which items are visible in the viewport and renders only those items. This technology ensures that as the user scrolls, the rendered DOM updates to display only necessary items while maintaining smooth performance.
introduce useVirtualizedList
Our custom hooks, useVirtualizedList
simplified virtual scrolling. It accepts a list of items, the height of each item, and the height of the container. This hook returns the visible items, positioning offset, and scroll handler to manage updates.
Implementation: Construct 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
-
Status management:
-
startIndex
andendIndex
Determine which items in the list should be visible. - These indexes are dynamically updated based on the scroll position.
-
-
Initial settings:
- this
useEffect
initializationendIndex
Based on the height of the container and the height of the item.
- this
-
scroll handler:
- this
handleScroll
Function Compute NewstartIndex
based on scroll position (scrollTop
) and updatestartIndex
andendIndex
therefore.
- this
-
offset:
-
offsetTop
: The total height of the item above the visible area. -
offsetBottom
: The total height of the items below the visible area.
-
Using Hooks in Components
This is an example of how to integrate useVirtualizedList
Enter 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;
Main features in the example
-
dynamic height:
- this
offsetTop
andoffsetBottom
div ensures that the list scrolls correctly without rendering all items.
- this
-
Smooth scrolling:
- this
onScroll
The event updates the visible items based on the current scroll position.
- this
-
Seamless user experience:
- Only visible items are rendered, improving performance on large datasets.
test hook
Testing hooks is critical to ensure they handle edge cases. This is a usage example test 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);
});
});
focus
- Virtualized scrolling is critical for performance when rendering large lists.
- this
useVirtualizedList
Hooks simplify the implementation of virtualized scrolling in React. - Proper testing ensures that hooks handle various scenarios such as scrolling, empty lists, and dynamic updates.
By employing this technique, you can enhance the performance and user experience of your React applications when working with large data sets.