When rendering a large number of items, failing to consider performance can lead to laggy scrolling or even slow down the entire app. To address this issue, virtualization techniques were introduced, and among them, TanStack Virtual stands out for its excellent performance and flexibility.
In this article, we’ll explore how to build a responsive grid component with aspect ratio support using TanStack Virtual.
sample code : https://github.com/dango0812/tanstack-virtual-grid
website : https://tanstack-virtual-grid.vercel.app/
Although the official TanStack Virtual website offers many great examples, you probably didn’t find one that supports responsive grid layouts with aspect ratio handling.
website: https://tanstack.com/virtual/latest/docs/framework/react/examples/dynamic
There's also an excellent library built on top of TanStack called virtual-grid/react, but even that doesn’t handle aspect ratio calculations out of the box.
website: https://www.virtual-grid.com/
In this post, I’ll show you how to build a fully responsive virtualized grid using TanStack Virtual—with support for consistent aspect ratios like 1:1 or 16:9, making it truly adaptable across different screen sizes!
📌 Constants Definition
const BREAKPOINT = {
xs: 375,
sm: 640,
lg: 1024
};
const ITEM_RATIO = 16 / 9;
BREAKPOINT: Similar to media queries, these are breakpoints that determine how the grid layout changes based on screen width.
ITEM_RATIO: The aspect ratio of each item. On larger screens, it maintains a 16:9 ratio, while on smaller screens, it switches to a square shape.
📌 Row & Column Virtualization using useVirtualizer
const rowVirtualizer = useVirtualizer({
count: Math.ceil(items.length / columns),
getScrollElement: () => parentRef.current,
estimateSize: () => itemSize.height + gap.y,
overscan: 2
});
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columns,
getScrollElement: () => parentRef.current,
estimateSize: () => itemSize.width + gap.x,
overscan: 2
});
Both rows and columns are virtualized separately, so only visible elements are rendered.
estimateSize: Estimates the height/width of each item to calculate the total virtual scroll area.
overscan: Renders additional items outside the viewport for improved scroll performance.
📌 useResizeObserver
const { width: containerWidth } = useResizeObserver(parentRef);
Uses ResizeObserver to detect changes in the width of the parent container.
In a real project, you might want to create a useWindowSize hook instead, to track window size directly!
const handleResize = throttle(() => {
...
}, 200);
This function is triggered whenever the screen size changes.
It’s throttled for performance, and recalculates gap, column count, and item size, then updates the state.
📌 Layout Calculation Functions
Each function is used within handleResize and calculates layout based on the current screen width.
const getItemGap = (width) => {...}
const getColumnsCount = (width) => {...}
const getItemWidth = (width, columns, gapX) => {...}
const getItemHeight = (width, itemWidth) => {...}
getItemGap: Adjusts item gap (in px) based on breakpoints.
getColumnsCount: Dynamically changes the number of columns according to width.
getItemWidth: Calculates item width by subtracting total gap from container width.
getItemHeight: Calculates height based on aspect ratio. Uses square ratio on small screens.
📌 Calling tanstack measure function
useEffect(() => {
rowVirtualizer.measure();
columnVirtualizer.measure();
}, [itemSize.height, columns]);
When item size or column count changes, re-measures the virtualizer’s internal layout.
📌 Rendering
{rowVirtualizer.getVirtualItems().map((virtualRow) =>
columnVirtualizer.getVirtualItems().map((virtualColumn) => {
...
})
)}
Nested loop structure iterates through both rows and columns.
Each item’s position is calculated using translateX and translateY (a required technique in virtual scrolling).
itemIndex is used to determine which item from the data array should be rendered.
Top comments (0)