I have the following component which calls api on scroll to keep adding images.
It has masonry layout similar to Pinterest. See image below on how it looks vs what I want.
Essentially images with different height, and the layout keeps them properly aligned with consistent margins.
I want the images to be added row by row. This is cos as the api loads data on scroll, I do not want the styling to keep readjusting the image positions.
This could have been styled differently going column by column which is an easy approach.
But is not efficient due to the api loading more data on scroll and images keeps readjusting which is not ideal.
So coming back to displaying images row by row, the following works, somewhat.
But the top calculation doesn’t seems to be accurate. At times is fine.
But quite regularly the calculation is off.
Can see in images below, the top distance between images varies. Whereas I want it to be consistent 16px.
What am I doing wrong with my top calculation? Pls advice. Thanks.
This is taking inspiration from the suggestion here.
https://stackoverflow.com/a/7128902/2840178
import React, {useState, useEffect, useRef, createRef} from 'react';
import axios from 'axios';
import InfiniteScroll from 'react-infinite-scroll-component';
import debounce from 'lodash.debounce';
const Masonry = () => {
const gutter = 16;
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [columnsPerRow, setColumnsPerRow] = useState(1);
const [columnHeights, setColumnHeights] = useState([0, 0, 0, 0]);
const divRefs = useRef([]);
const updateColumns = () => {
const parentContainer = document.getElementById('mason-parent');
if (parentContainer) {
const containerWidth = parentContainer.clientWidth;
const columnWidth = 400;
const noOfColumnsPerRow = Math.floor(containerWidth / columnWidth);
const cols = Math.max(1, noOfColumnsPerRow);
setColumnsPerRow(cols);
setColumnHeights(Array(cols).fill(0));
}
};
useEffect(() => {
fetchItems();
updateColumns();
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', updateColumns);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', updateColumns);
};
}, []);
const fetchItems = async () => {
try {
const page = 5;
const response = await axios.get(`http://localhost:5005/api?page=${page}&perPage=30`);
const newItems = response.data;
setItems((prevItems) => [...prevItems, ...newItems]);
if (newItems.length === 0) {
setHasMore(false);
}
setPage((prevPage) => prevPage + 1);
} catch (error) {
console.error('Error fetching data:', error);
}
};
const debouncedFetchItems = debounce(fetchItems, 500);
const handleScroll = () => {
const scrollY = window.scrollY || window.pageYOffset;
if (scrollY + window.innerHeight >= document.documentElement.scrollHeight - 200) {
debouncedFetchItems();
}
};
const handleImageLoad = (index) => {
const divRef = divRefs[index];
if (divRef && divRef.current) {
const divHeight = divRef.current.clientHeight;
const columnIndex = index % columnsPerRow;
const left = (columnIndex * (400 + gutter)) + gutter;
setColumnHeights((prevHeights) => {
const newHeights = [...prevHeights];
const shortestColumnIndex = newHeights.indexOf(Math.min(...newHeights));
divRef.current.style.top = `${newHeights[shortestColumnIndex]}px`;
newHeights[shortestColumnIndex] += (divHeight + gutter);
return newHeights;
});
divRef.current.style.left = `${left}px`;
}
};
return (
<div>
<div id="mason-parent" style={{ position: "relative", maxWidth: '1680px', marginLeft: 'auto', marginRight: 'auto'}}>
<InfiniteScroll
dataLength={items.length}
next={fetchItems}
hasMore={hasMore}
loader={<h4>Loading...</h4>}
endMessage={<p>No more items</p>}
>
{items.map((imageUrl, index) => {
if (!divRefs[index]) {
divRefs[index] = createRef();
}
return (
<div key={index} style={{position: 'absolute'}} ref={divRefs[index]}>
<img src={imageUrl} alt={`Image ${index + 1}`} loading='lazy' onLoad={() => handleImageLoad(index)}/>
</div>
);
})}
</InfiniteScroll>
</div>
</div>
);
};
export default Masonry;
This is what I want consistently, the top has uniform 16px margins. As indicated by all the red rectangles.

But I end up with inconsistent top as follows intermittently.
