🧱 masonic
npm i masonic
A performant and versatile virtualized masonry grid for React based on Brian Vaughn's react-virtualized
and further inspired by react-window.
Using Masonic, you're not just getting a component. You're getting the implementation details, as well, meaning advanced
usage requiring little code is possible.
Features
Quick Start
Check out the demo on CodeSandbox
import * as React from "react";
import { Masonry } from "masonic";
let i = 0;
const items = Array.from(Array(5000), () => ({ id: i++ }));
const EasyMasonryComponent = (props) => (
<Masonry items={items} render={MasonryCard} />
);
const MasonryCard = ({ index, data: { id }, width }) => (
<div>
<div>Index: {index}</div>
<pre>ID: {id}</pre>
<div>Column width: {width}</div>
</div>
);
Documentation
Components
Component | Description |
---|
<Masonry> | A "batteries included" masonry grid which includes all of the implementation details below. This component is the easiest way to get off and running in your app, before switching to more advanced implementations, if necessary. It will change its column count to fit its container's width and will decide how many rows to render based upon the height of the browser window . |
<MasonryScroller> | A heavily-optimized component that updates useMasonry() when the scroll position of the browser window changes. This bare-metal component is used by <Masonry> above. |
<List> | This is just a single-column <Masonry> component with no columnGutter prop, only rowGutter . |
Hooks
Hook | Description |
---|
useMasonry() | This hook handles the render phases of the masonry layout and returns the grid as a React element. |
usePositioner() | This hook creates the grid cell positioner and cache required by useMasonry() . This is the meat of the grid's layout algorithm, determining which cells to render at a given scroll position, as well as where to place new items in the grid. |
useResizeObserver() | Creates a resize observer that forces updates to the grid when mutations are made to the grid cells affecting their size. |
useContainerPosition() | A hook for measuring the width of the grid container, as well as its distance from the top of the document. These values are necessary to correctly calculate the number/width of columns to render, as well as the number of rows to render. |
useScroller() | A hook for tracking whether the window is currently being scrolled and it's scroll position on the y-axis. These values are used for determining which grid cells to render and when to add styles to the grid container that maximize scroll performance. |
useScrollToIndex() | A hook that creates a callback for scrolling to a specific index in the "items" array. |
useInfiniteLoader() | A utility hook for seamlessly adding infinite scroll behavior to the useMasonry() hook. This hook invokes a callback each time the last rendered index surpasses the total number of items in your items array or the number defined in the totalItems option of this hook. |
Utilities
Recipes
<Masonry>
An autosizing masonry grid that only renders items currently visible in the browser window
. This
component will change its column count to fit its container's width and will decide how many rows
to render based upon the height of the browser window
. To facilitate this, it uses useMasonry()
,
usePositioner()
, useResizeObserver()
,
useContainerPosition()
, and useScroller()
under the hood.
This is the "batteries included" option. It's the easiest way to get off and running with your app and a
great stepping stone to more advanced implementations, should you need them.
Check out an example on CodeSandbox
import * as React from "react";
import { Masonry } from "masonic";
let i = 0;
const items = Array.from(Array(5000), () => ({ id: i++ }));
const EasyMasonryComponent = (props) => (
<Masonry items={items} render={MasonryCard} />
);
const MasonryCard = ({ index, data: { id }, width }) => (
<div>
<div>Index: {index}</div>
<pre>ID: {id}</pre>
<div>Column width: {width}</div>
</div>
);
Props
Required props
Prop | Type | Required? | Description |
---|
items | any[] | Yes | An array containing the data used by the grid items. |
render | React.ComponentType<RenderComponentProps> | Yes | This component is rendered for each item of your items prop array. It should accept three props: index , width , and data . See RenderComponentProps for more detail on those props. |
Column props
Props for tuning the column width, count, and gutter of your component.
Prop | Type | Default | Required? | Description |
---|
columnWidth | number | 240 | No | This is the minimum column width. Masonic will automatically size your columns to fill its container based on your provided columnWidth and columnGutter values. It will never render anything smaller than this defined width unless its container is smaller than its value. |
columnGutter | number | 0 | No | This sets the horizontal space between grid columns in pixels. If rowGutter is not set, this also sets the vertical space between cells within a column in pixels. |
rowGutter | number | Same as columnGutter | No | This sets the vertical space between cells within a column in pixels. |
columnCount | number | | No | By default, Masonic derives the column count from the columnWidth prop. However, in some situations it is nice to be able to override that behavior e.g. when creating a <List> . |
Grid container props
These props customize how the grid container element is rendered.
Prop | Type | Default | Required? | Description |
---|
as | keyof JSX.IntrinsicElements | React.ComponentType<any> | "div" | No | This is the type of element the grid container will be rendered as. |
id | string | | No | Gives the grid container an id . |
className | string | | No | Gives the grid container a className . |
style | React.CSSProperties | | No | Adds extra style attributes to the container in addition to those created by the useMasonry() hook. |
role | "grid" | "list" | "grid" | No | Optionally swap out the accessibility role prop of the container and its items. |
tabIndex | number | 0 | No | Change the tabIndex of the grid container. |
Grid item props
Props that customize how individual grid item containers are rendered.
Prop | Type | Default | Required? | Description |
---|
itemAs | keyof JSX.IntrinsicElements | React.ComponentType<any> | "div" | No | This is the type of element the grid items will be rendered as. |
itemStyle | React.CSSProperties | | No | Adds extra style attributes to the grid items in addition to those created by the useMasonry() hook. |
itemHeightEstimate | number | 300 | No | This value is used for estimating the initial height of the masonry grid. It is important for the UX of the scrolling behavior and in determining how many items to render in a batch, so it's wise to set this value with some level accuracy, though it doesn't need to be perfect. |
itemKey | (data: any, index: number) => string | number | (data, index) => index | No | The value returned here must be unique to the item. By default, the key is the item's index. This is ok if your collection of items is never modified. Setting this property ensures that the component in render is reused each time the masonry grid is reflowed. A common pattern would be to return the item's database ID here if there is one, e.g. data => data.id |
Callbacks
Prop | Type | Default | Required? | Description |
---|
onRender | (startIndex: number, stopIndex: number, items: any[]) => void | | No | This callback is invoked any time the items currently being rendered by the grid change. See onRender() arguments. |
Other props
Prop | Type | Default | Required? | Description |
---|
overscanBy | number | 2 | No | This number is used for determining the number of grid cells outside of the visible window to render. The default value is 2 which means "render 2 windows worth (2 * height ) of content before and after the items in the visible window". A value of 3 would be 3 windows worth of grid cells, so it's a linear relationship. Overscanning is important for preventing tearing when scrolling through items in the grid, but setting too high of a value may create too much work for React to handle, so it's best that you tune this value accordingly. |
scrollToIndex | number | {index: number, align: "top" | "center" | "bottom"} | | No | Scrolls to a given index within the grid. The grid will re-scroll any time the index changes. |
onRender()
arguments
Argument | Type | Description |
---|
startIndex | number | The index of the first item currently being rendered in the window |
stopIndex | number | The index of the last item currently being rendered in the window |
items | any[] | The array of items you provided to the items prop |
RenderComponentProps
Prop | Type | Description |
---|
index | number | The index of the cell in the items prop array. |
width | number | The rendered width of the cell's column. |
data | any | The data at items[index] of your items prop array.. |
<MasonryScroller>
A heavily-optimized component that updates useMasonry()
when the scroll position of the
browser window
changes. This bare-metal component is used by <Masonry>
above.
When would you use this? If you're building an advanced masonry grid implementation, but you don't want
to deal with figuring out how to optimize the exchange between scroll position changes in the browser
window
and the useMasonry() hook
.
Check out an example on CodeSandbox
import * as React from "react";
import { MasonryScroller, usePositioner, useContainerPosition } from "masonic";
import { useWindowSize } from "@react-hook/window-size";
const MyMasonry = (props) => {
const containerRef = React.useRef(null);
const [windowWidth, windowHeight] = useWindowSize();
const { offset, width } = useContainerPosition(containerRef, [
windowWidth,
windowHeight,
]);
const positioner = usePositioner({ width, columnWidth: 320 });
return (
<MasonryScroller
positioner={positioner}
// The distance in px between the top of the document and the top of the
// masonry grid container
offset={offset}
// The height of the virtualization window
height={windowHeight}
// Forwards the ref to the masonry container element
containerRef={containerRef}
{...props}
/>
);
};
Props
In addition to these props, this component accepts all of the props outlined in <Masonry>
with exception to columnGutter
, rowGutter
, columnWidth
, columnCount
, ssrWidth
, and ssrHeight
.
Prop | Type | Default | Required? | Description |
---|
offset | number | 0 | No | The vertical space in pixels between the top of the grid container and the top of the browser document.documentElement . |
height | number | | Yes | This is the height of the window. If you're rendering the grid relative to the browser window , the current document.documentElement.clientHeight is the value you'll want to set here. If you're rendering the grid inside of another HTML element, you'll want to provide the current element.offsetHeight here. |
positioner | Positioner | | Yes | A grid cell positioner and cache created by the usePositioner() hook or createPositioner() utility. |
containerRef | React.MutableRefObject<HTMLElement | null> | | No | Forwards a React ref to the grid container. |
resizeObserver | ResizeObserver | | No | A resize observer that tracks mutations to the grid cells and forces the Masonry grid to recalculate its layout if any cells affect column heights change. Check out the useResizeObserver() hook and createResizeObserver() utility. |
<List>
This is a single-column <Masonry>
component. That is, it uses the useMasonry()
hook
and other utilities to create a virtualized list.
Check out an example on CodeSandbox
import * as React from "react";
import { List } from "masonic";
let i = 0;
const items = Array.from(Array(5000), () => ({ id: i++ }));
const EasyListComponent = (props) => (
<List items={items} rowGutter={32} render={ListCard} />
);
const ListCard = ({ index, data: { id }, width }) => (
<div>
<div>Index: {index}</div>
<pre>ID: {id}</pre>
<div>Column width: {width}</div>
</div>
);
Props
In addition to these props, this component accepts all of the props outlined in <Masonry>
with exception to columnGutter
, columnWidth
, and columnCount
.
Prop | Type | Default | Required? | Description |
---|
rowGutter | number | 0 | No | The amount of vertical space in pixels to add between list item cards. |
useMasonry(options)
This hook handles the render phases of the masonry layout and returns the grid as a React element.
Check out an example on CodeSandbox
import * as React from "react";
import { useWindowSize } from "@react-hook/window-size";
import {
useMasonry,
usePositioner,
useContainerPosition,
useScroller,
} from "masonic";
const MyMasonry = (props) => {
const containerRef = React.useRef(null);
const [windowWidth, height] = useWindowSize();
const { offset, width } = useContainerPosition(containerRef, [
windowWidth,
height,
]);
const { scrollTop, isScrolling } = useScroller(offset);
const positioner = usePositioner({ width });
return useMasonry({
positioner,
scrollTop,
isScrolling,
height,
containerRef,
...props,
});
};
Arguments
Argument | Type | Description |
---|
options | UseMasonryOptions | The distance in pixels between the top of your masonry container and the top of the document |
UseMasonryOptions
Required options
Prop | Type | Required? | Description |
---|
items | any[] | Yes | An array containing the data used by the grid items. |
positioner | Positioner | Yes | A grid cell positioner and cache created by the usePositioner() hook or createPositioner() utility. |
height | number | Yes | This is the height of the window. If you're rendering the grid relative to the browser window , the current document.documentElement.clientHeight is the value you'll want to set here. If you're rendering the grid inside of another HTML element, you'll want to provide the current element.offsetHeight here. |
scrollTop | number | Yes | The current scroll progress in pixel of the window the grid is rendered in. If you're rendering the grid relative to the browser window , you'll want the most current window.scrollY here. If you're rendering the grid inside of another HTML element, you'll want the current element.scrollTop value here. The useScroller() hook and <MasonryScroller> components will help you if you're rendering the grid relative to the browser window . |
render | React.ComponentType<RenderComponentProps> | Yes | This component is rendered for each item of your items prop array. It should accept three props: index , width , and data . See RenderComponentProps . |
Grid container options
Prop | Type | Default | Required? | Description |
---|
as | keyof JSX.IntrinsicElements | React.ComponentType<any> | "div" | No | This is the type of element the grid container will be rendered as. |
id | string | | No | Optionally gives the grid container an id prop. |
className | string | | No | Optionally gives the grid container a className prop. |
style | React.CSSProperties | | No | Adds extra style attributes to the container in addition to those created by the useMasonry() hook. |
role | "grid" | "list" | "grid" | No | Optionally swap out the accessibility role prop of the container and its items. |
tabIndex | number | 0 | No | Change the tabIndex of the grid container. |
containerRef | React.MutableRefObject<HTMLElement | null> | | No | Forwards a React ref to the grid container. |
Grid item options
Prop | Type | Default | Required? | Description |
---|
itemAs | keyof JSX.IntrinsicElements | React.ComponentType<any> | "div" | No | This is the type of element the grid items will be rendered as. |
itemStyle | React.CSSProperties | | No | Adds extra style attributes to the grid items in addition to those created by the useMasonry() hook. |
itemHeightEstimate | number | 300 | No | This value is used for estimating the initial height of the masonry grid. It is important for the UX of the scrolling behavior and in determining how many items to render in a batch, so it's wise to set this value with some level accuracy, though it doesn't need to be perfect. |
itemKey | (data: any, index: number) => string | number | (data, index) => index | No | The value returned here must be unique to the item. By default, the key is the item's index. This is ok if your collection of items is never modified. Setting this property ensures that the component in render is reused each time the masonry grid is reflowed. A common pattern would be to return the item's database ID here if there is one, e.g. data => data.id |
Callbacks
Prop | Type | Default | Required? | Description |
---|
onRender | (startIndex: number, stopIndex: number | undefined, items: any[]) => void | | No | This callback is invoked any time the items currently being rendered by the grid change. |
Other options
Prop | Type | Default | Required? | Description |
---|
overscanBy | number | 2 | No | This number is used for determining the number of grid cells outside of the visible window to render. The default value is 2 which means "render 2 windows worth (2 * height ) of content before and after the items in the visible window". A value of 3 would be 3 windows worth of grid cells, so it's a linear relationship. Overscanning is important for preventing tearing when scrolling through items in the grid, but setting too high of a value may create too much work for React to handle, so it's best that you tune this value accordingly. |
resizeObserver | ResizeObserver | | No | A resize observer that tracks mutations to the grid cells and forces the Masonry grid to recalculate its layout if any cells affect column heights change. Check out the useResizeObserver() hook and createResizeObserver() utility. |
usePositioner(options, deps?)
This hook creates the grid cell positioner and cache required by useMasonry()
. This is
the meat of the grid's layout algorithm, determining which cells to render at a given scroll
position, as well as where to place new items in the grid.
Check out an example on CodeSandbox
import * as React from "react";
import { usePositioner, useContainerPosition, MasonryScroller } from "masonic";
const MyMasonry = ({ columnWidth = 300, columnGutter = 16, ...props }) => {
const { width, offset } = useContainerPosition();
const positioner = usePositioner({ width, columnWidth, columnGutter });
return <MasonryScroller positioner={positioner} offset={offset} {...props} />;
};
Arguments
Argument | Type | Default | Required? | Description |
---|
options | UsePositionerOptions | | Yes | Properties that determine the number of columns in the grid, as well as their widths. |
deps | React.DependenciesList | [] | No | This hook will create a new positioner, clearing all existing cached positions, whenever the dependencies in this list change. |
UsePositionerOptions
Argument | Type | Default | Required? | Description |
---|
width | number | | Yes | The width of the container you're rendering the grid within, e.g. the container element's element.offsetWidth . That said, you can provide any width here. |
columnWidth | number | 200 | No | The minimum column width. The usePositioner() hook will automatically size the columns to fill their container based upon the columnWidth and columnGutter values. It will never render anything smaller than this width unless its container itself is smaller than its value. This property has no effect if you're providing a columnCount . |
columnGutter | number | 0 | No | This sets the horizontal space between grid columns in pixels. If rowGutter is not set, this also sets the vertical space between cells within a column in pixels. |
rowGutter | number | Same as columnGutter | No | This sets the vertical space between cells within a column in pixels. |
columnCount | number | | No | By default, usePositioner() derives the column count from the columnWidth , columnGutter , and width props. However, in some situations it is nice to be able to override that behavior (e.g. creating a <List> -like component). |
Positioner
export interface Positioner {
columnCount: number;
columnWidth: number;
set: (index: number, height: number) => void;
get: (index: number) => PositionerItem | undefined;
update: (updates: number[]) => void;
range: (
lo: number,
hi: number,
renderCallback: (index: number, left: number, top: number) => void
) => void;
size: () => number;
estimateHeight: (itemCount: number, defaultItemHeight: number) => number;
shortestColumn: () => number;
all: () => PositionerItem[];
}
export interface PositionerItem {
top: number;
left: number;
height: number;
column: number;
}
useScroller(offset?, fps?)
A hook for tracking whether the window
is currently being scrolled and it's scroll position
on the y-axis. These values are used for determining which grid cells to render and when
to add styles to the masonry container that maximize scroll performance.
Check out an example on CodeSandbox
import * as React from "react";
import { useMasonry, usePositioner, useScroller } from "masonic";
const MyMasonry = (props) => {
const containerRef = React.useRef(null);
const { offset, width } = useContainerPosition(containerRef);
const positioner = usePositioner({ width });
const { scrollTop, isScrolling } = useScroller(offset);
return useMasonry({
...props,
containerRef,
positioner,
scrollTop,
isScrolling,
});
};
Arguments
Argument | Type | Description |
---|
offset | number | The vertical space in pixels between the top of the grid container and the top of the browser document.documentElement . |
fps | number | This determines how often (in frames per second) to update the scroll position of the browser window in state, and as a result the rate the masonry grid recalculates its visible cells. The default value of 12 has been very reasonable in my own testing, but if you have particularly heavy render components it may be prudent to reduce this number. |
Returns {scrollTop: number; isScrolling: boolean}
useContainerPosition(elementRef, deps?)
A hook for measuring the width of the grid container, as well as its distance
from the top of the document. These values are necessary to correctly calculate the number/width
of columns to render, as well as the number of rows to render.
Check out an example on CodeSandbox
import * as React from "react";
import { useWindowSize } from "@react-hook/window-size";
import { useContainerPosition, MasonryScroller } from "masonic";
const MyMasonry = (props) => {
const containerRef = React.useRef(null);
const [windowWidth, windowHeight] = useWindowSize();
const { offset, width } = useContainerRect(
containerRef,
[windowWidth, windowHeight]
);
return (
<MasonryScroller
width={width}
height={windowHeight}
containerRef={containerRef}
{...props}
/>
);
};
Arguments
Argument | Type | Description |
---|
elementRef | number | A ref object created by React.useRef() . That ref should be provided to the containerRef property in useMasonry() . |
deps | React.DependenciesList | You can force this hook to recalculate the offset and width whenever this dependencies list changes. A common dependencies list might look like [windowWidth, windowHeight] , which would force the hook to recalculate any time the size of the browser window changed. |
ContainerPosition
export interface ContainerPosition {
offset: number;
width: number;
}
useScrollToIndex(positioner, options?)
A hook that creates a callback for scrolling to a specific index in the "items" array.
Check out an example on CodeSandbox
import * as React from "react";
import {
useMasonry,
usePositioner,
useContainerPosition,
useScroller,
useScrollToIndex,
} from "masonic";
const MyMasonry = (props) => {
const containerRef = React.useRef(null);
const { offset, width } = useContainerPosition(containerRef);
const { scrollTop, isScrolling } = useScroller(offset);
const positioner = usePositioner({ width });
const scrollToIndex = useScrollToIndex(positioner);
React.useEffect(() => {
if (props.scrollToIndex) {
scrollToIndex(props.scrollToIndex);
}
}, [props.scrollToIndex, scrollToIndex]);
return useMasonry({
...props,
containerRef,
positioner,
scrollTop,
isScrolling,
});
};
Arguments
UseScrollToIndexOptions
Option | Type | Description |
---|
element | Window | HTMLElement | React.RefObject<HTMLElement> | null | The window element or a React ref for the window element. That is, this is the grid container. |
align | "top" | "center" | "bottom" | Sets the vertical alignment of the cell within the grid container. |
height | number | The height of the grid. |
offset | number | The vertical space in pixels between the top of the grid container and the top of the window. |
Returns (index: number) => void
useResizeObserver(positioner)
This hook creates a resize observer that forces updates to the grid cell positions when mutations are
made to cells affecting their height.
Check out an example on CodeSandbox
import * as React from "react";
import { useMasonry, usePositioner, useResizeObserver } from "masonic";
const MyMasonry = (props) => {
const positioner = usePositioner({ width: 1024 });
const resizeObserver = useResizeObserver(positioner);
return useMasonry({
positioner,
resizeObserver,
scrollTop,
isScrolling,
height,
...props,
});
};
Arguments
useInfiniteLoader(loadMoreItems, options?)
A utility hook for seamlessly adding infinite scroll behavior to the useMasonry()
hook
and the components that use it. This hook invokes a callback each time the last rendered index surpasses
the total number of items in your items array or the number defined in the totalItems
option.
Check out an example on CodeSandbox
import * as React from "react";
import { Masonry, useInfiniteLoader } from "masonic";
const InfiniteMasonry = (props) => {
const [items, setItems] = useState([
]);
const fetchMoreItems = async (startIndex, stopIndex, currentItems) => {
const nextItems = await fetch(
`/api/get-more?after=${startIndex}&limit=${startIndex + stopIndex}`
);
setItems((current) => [...current, ...nextItems]);
};
const maybeLoadMore = useInfiniteLoader(fetchMoreItems, {
isItemLoaded: (index, items) => !!items[index],
});
return <Masonry {...props} items={items} onRender={maybeLoadMore} />;
};
Arguments
Argument | Type | Description |
---|
loadMoreItems | (startIndex: number, stopIndex: number, items: any[]) => any | This callback is invoked when more rows must be loaded. It will be used to determine when to refresh the list with the newly-loaded data. |
options | UseInfiniteLoaderOptions | Configuration object for your loader, see UseInfiniteLoaderOptions below. |
UseInfiniteLoaderOptions
Property | Type | Default | Description |
---|
isItemLoaded | (index: number, items: any[]) => boolean | (index, items) => items[index] !== undefined | A callback responsible for determining the loaded state of each item. Should return true if the item has already been loaded and false if not. |
minimumBatchSize | number | 16 | The minimum number of new items to be loaded at a time. This property can be used to batch requests and reduce HTTP requests. |
threshold | number | 16 | The threshold at which to pre-fetch data. A threshold X means that new data should start loading when a user scrolls within X cells of the end of your items array. |
totalItems | number | 9E9 | The total number of items you'll need to eventually load (if known). This can be arbitrarily high if not known. |
Returns (startIndex: number, stopIndex: number, items: any[]) => any
createPositioner(columnCount, columnWidth, columnGutter, rowGutter)
Creates a cell positioner for the useMasonry()
hook. The usePositioner()
hook uses
this utility under the hood.
Arguments
Argument | Type | Description |
---|
columnCount | number | The number of columns in the grid |
columnWidth | number | The width of each column in the grid |
columnGutter | number | The amount of horizontal space between columns in pixels. |
rowGutter | number | The amount of vertical space between cells within a column in pixels (falls back to columnGutter ). |
createResizeObserver(positioner, updater)
Creates a resize observer that forces updates to the grid cell positions when mutations are
made to cells affecting their height.
Arguments
Argument | Type | Description |
---|
positioner | Positioner | A cell positioner created by the usePositioner() hook or the createPositioner() utility |
updater | (updates: number[]) => void | A callback that fires whenever one or many cell heights change. Updates are provided to the callback in the form of a flat array: [index, height, index, height, index, height, ...] |
There are actually quite a few differences between these components and
the originals, despite the overall design being highly inspired by them.
-
The react-virtualized
component requires a <CellMeasurer>
,
cellPositioner
, and cellMeasurerCache
and a ton of custom implementation
to get off the ground. It's very difficult to work with. In Masonic
this
functionality is built in using resize-observer-polyfill
for tracking cell size changes.
-
This component can auto-calculate the number of columns to render based
upon the defined columnWidth
property. The column count will update
any time it changes.
-
The algoirthm for updating cell positions and sizes is much more
efficient in this component because only specific cells and columns are
updated when cell sizes change, whereas in the original a complete reflow
has to triggered.
-
The API and internals are a complete rewrite and because of the above points, is
much easier to use in my opinion.
Legacy Documentation
LICENSE
MIT