another-react-responsive-masonry
v0.1.0-alpha.3
Published
A React virtualized responsive masonry library built on top of @tanstack/virtual
Downloads
7
Maintainers
Readme
another-react-responsive-masonry
A React virtualized responsive masonry library built on top of @tanstack/virtual (commit: 94761ff)
Demo video:
https://github.com/user-attachments/assets/39194a80-004f-4877-9e31-dad65f751ba9
Why Another Masonry Library?
A truly effective masonry layout needs three critical features:
- 📱 Responsive - Adapts to different screen sizes with varying column counts
- ⚡ Virtualized - Only renders visible items for optimal performance with large datasets
- ⚖️ Height-balanced - Distributes items intelligently across columns to maintain visual balance
Most existing React masonry libraries fall short in one or more of these areas. The majority use simple CSS Grid or Flexbox approaches, which merely assign items to columns in sequence without considering height distribution. Some offer virtualization, but very few actually measure and calculate item heights to balance layouts effectively.
This library builds upon @tanstack/virtual, which already handles virtualization and height balancing (the most technically challenging features). However, @tanstack/virtual lacks proper responsive support - changing the number of lanes on window resize causes issues. By extending it with custom modifications, this library has tried its best to solve all three requirements.
Design decision: Instead of providing a high-level, opinionated component, this library exports separate low-level hooks to give developers maximum flexibility and control. They're the same as original library's hooks with minimal adjustments to retain the powerful customization capabilities.
Key Modules
useBreakpoint
Located in packages/lib/src/hooks/useBreakpoint.tsx, this hook manages responsive breakpoints by:
- Accepting an array of breakpoint configurations with
minWidthandnCol(number of columns) - Using
window.matchMediato detect the current breakpoint - Providing debounced updates to prevent excessive re-renders during window resizing
- Exporting a context provider for sharing breakpoint state across components
Modified @tanstack/virtual
The packages/lib/src/tanstack/ directory contains the modified source code from @tanstack/virtual. The patch file shows the exact changes made:
Major modifications:
Per-layout measurement caching - The biggest architectural change is how item measurements are stored. Each unique number of lanes (columns) is treated as a separate layout, with its own independent measurement cache (
lanesCachearray). This ensures that when the window is resized but stays within the same breakpoint (same number of lanes), items remain in their assigned lanes without recalculation.Stable lane assignment - Within the same layout, each item's lane assignment remains constant across resizes, preventing jarring layout shifts.
Updated
getVirtualItemsreturn - Now returns{ virtualItems, lanes }instead of just the items array, providing access to the current number of lanes.Debounced resize handling - Added
resizeDelayoption that debounces re-renders during window resize, improving performance and reducing visual flickering.Enhanced
estimateSizefunction - The function signature now includes the number of lanes as a parameter:estimateSize(index, lanes). This allows more accurate initial size estimates based on the current column configuration. Providing a good estimate function is crucial for optimal performance.
Usage
npm install another-react-responsive-masonrySee the example source code in packages/example/src for a better understanding of how to use the library.
Usage is nearly identical to @tanstack/virtual. In most cases you can easily import it as a drop-in replacement and keep most of your existing code while gaining good responsive masonry behavior.
Quick Start:
import { useBreakpoint, useWindowVirtualizer, VirtualItem } from 'another-react-responsive-masonry';
const breakpointColumns = [
{ name: 'mobile', minWidth: 0, nCol: 1 },
{ name: 'small', minWidth: 480, nCol: 2 },
{ name: 'medium', minWidth: 768, nCol: 3 },
{ name: 'large', minWidth: 1024, nCol: 4 },
{ name: 'xlarge', minWidth: 1280, nCol: 5 },
];
function useMasonry(items: CardItem[], rowGap: number) {
const { currentBreakpoint } = useBreakpoint(breakpointColumns);
const columnRef = useRef<HTMLDivElement>(null);
const [enabled, setEnabled] = useState(false);
useEffect(() => {
// Only enable after elements are mounted
setEnabled(true);
}, []);
const rowVirtualizer = useWindowVirtualizer({
enabled,
count: items.length,
overscan: 10,
scrollMargin: columnRef.current?.offsetTop,
lanes: currentBreakpoint.nCol,
gap: rowGap,
useAnimationFrameWithResizeObserver: true,
resizeDelay: 50,
estimateSize: (i) => {
return items[i].estimateHeight(columnRef.current?.clientWidth || 300);
},
});
const { virtualItems, lanes } = rowVirtualizer.getVirtualItems();
const columns = useMemo(() => {
const arr = Array.from({ length: lanes }, () => [] as VirtualItem[]);
for (const item of virtualItems) {
arr[item.lane].push(item);
}
return arr;
}, [virtualItems, lanes]);
return { columnRef, columns, rowVirtualizer };
}
function MasonryContainer() {
const [items, setItems] = useState<CardItem[]>(() => generateSampleItems(50));
const rowGap = 10;
const columnGap = 20;
const { columnRef, columns, rowVirtualizer } = useMasonry(items, rowGap);
return (
<>
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
gap: columnGap,
width: '100%',
height: rowVirtualizer.getTotalSize(),
}}
>
{columns.map((column, index) => (
<div
ref={index === 0 ? columnRef : undefined}
key={index}
style={{
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
{column.map((virtualItem) => {
const { estimateHeight: _, ...props } = items[virtualItem.index];
return (
<Card
key={props.id}
measureElement={rowVirtualizer.measureElement}
dataIndex={virtualItem.index}
offsetY={virtualItem.start - rowVirtualizer.options.scrollMargin}
{...props}
/>
);
})}
</div>
))}
</div>
</>
);
}License
MIT
