DEV Community

Cover image for How to build a responsive virtual grid with tanStack virtual
dango0812
dango0812

Posted on

How to build a responsive virtual grid with tanStack virtual

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;
Enter fullscreen mode Exit fullscreen mode

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
    });
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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) => {...}
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

When item size or column count changes, re-measures the virtualizer’s internal layout.

📌 Rendering

{rowVirtualizer.getVirtualItems().map((virtualRow) =>
    columnVirtualizer.getVirtualItems().map((virtualColumn) => {
        ...
    })
)}
Enter fullscreen mode Exit fullscreen mode

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)