🧱 masonic
npm i masonic
A virtualized masonry grid component for React based
on Brian Vaughn's react-virtualized
and further inspired by react-window.
Features
- Easy to use It only takes two minutes to start creating your own masonry grid with this component.
For reals, check out the demo on CodeSandbox.
- Blazing™ fast This component can seamlessly render hundreds of thousands of grid items
without issue via virtualization and intelligent data structures. It uses a red black interval tree
to determine which grid items with
O(log n + m)
lookup performance to render based upon the scroll position and size of the window. - TypeScript Woohoo, superior autocomplete and type safety means fewer bugs in your implementation.
- Versatility All of the autosizing
<Masonry>
's constituent parts are provided via exports so you're
not locked into to the implementation. At times it will be useful to have access to those internals. It's also
possible to kick the virtualization out of the equation by providing an infinite value to the overscanBy
prop, though
this would be a terrible idea for large lists. - Autosizing The grid will automatically resize itself and its items if the content of the
grid items changes or resizes. For example, when an image lazily loads this component will
automatically do the work of recalculating the size of that grid item using
resize-observer-polyfill
.
Quick Start
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>
)
API
Components
Component | Description |
---|
<Masonry> | An autosizing masonry grid component that only renders items currently viewable in the 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 window . To facilitate this, it uses <FreeMasonry> , useContainerRect() , and useWindowScroller() under the hood. |
<FreeMasonry> | A more flexible masonry grid component that lets you define your own width , height , scrollTop , and isScrolling props. |
<List> | This is just a single-column <Masonry> component. |
Hooks
Hook | Description |
---|
useInfiniteLoader() | A utility hook for seamlessly adding infinite scroll behavior to the <Masonry> component. 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. |
useContainerRect() | A hook used for measuring and tracking the width of the masonry component's container, as well as its distance from the top of your document. These values are necessary to correctly calculate the number/width of columns to render, as well as the number of rows to render. |
useWindowScroller() | A hook used for measuring the size of the browser window, whether or not the window is currently being scrolled, and the window's scroll position. These values are used when calculating the number of rows to render and determining when we should disable pointer events on the masonry container to maximize scroll performance. |
<Masonry>
An autosizing masonry grid component that only renders items currently viewable in the 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 window
. To facilitate this, it uses <FreeMasonry>
,
useContainerRect()
, and useWindowScroller()
under the hood.
Props
Columns
Props for tuning the column width, count, and gutter of your component.
Prop | Type | Default | Required? | Description |
---|
columnWidth | number | 240 | Yes | 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 amount (px) of vertical and horizontal space between grid items. |
columnCount | number | undefined | 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> . |
Item rendering
Props that dictate how individual grid items are rendered.
Prop | Type | Default | Required? | Description |
---|
render | React.ComponentClass|React.FC | undefined | Yes | The component provided here is rendered for each item of your items array (see below). The component here should handle the render props defined below. |
items | any[] | undefined | Yes | An array of items to render. The data contained at each index is passed to the data prop of your render component. It is also passed to the onRender callback and the itemKey generator. Its length is used for determining the estimated height of the container. |
itemHeightEstimate | number | 300 | No | This value is used for estimating the initial height of the masonry grid. it is vital to the UX of the scrolling behavior and in determining how many items to initially render, so its wise to set this value with some accuracy. |
itemAs | React.ReactNode | "div" | No | Your render component is wrapped with an element that has a style prop which sets the position of the grid item in its container. This is the type of element created for that wrapper. One common use case would be changing this property to li and the Masonry component's as prop to ul . |
itemStyle | React.CSSProperties | undefined | No | You can add additional styles to the wrapper discussed in itemAs by setting this property. |
itemKey | (data: any, index: number) => string | (_, 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 |
overscanBy | number | 2 | No | This number is used for determining the number of grid items outside of the visible window to render. The default value is 2 which means "render 2 windows worth of content before and after the items in the visible window". A value of 3 would be 3 windows worth of grid items, 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. |
render
props
These are the props provided to the component you set in your render
prop.
Prop | Type | Description |
---|
data | any | This is the data contained at items[index] of your items prop array. |
index | number | The index of the item in your items prop array. |
width | number | The width of the collumn containing this component. This is super useful for doing things like determining the dimensions of images. |
Customizing the container element
These props customize how the masonry grid element is rendered.
Prop | Type | Default | Required? | Description |
---|
as | React.ReactNode | "div" | No | This sets the element type of the masonry grid. A common use case would be changing this to ul and itemAs to li . |
id | string | undefined | No | Add an ID to the masonry grid container. |
className | string | undefined | No | Add a class to the masonry grid container. |
style | React.CSSProperties | undefined | No | Add inline styles to the masonry grid container. |
role | string | "grid" | No | Change the aria/a11y role of the container. |
tabIndex | number | 0 | No | Change the tabIndex of the container. By default the container is tabbable. |
Customizing the window for SSR
These are useful values to set when using SSR because in SSR land we don't have access to the
width and height of the window, and thus have no idea how many items to render.
Prop | Type | Default | Required? | Description |
---|
initialWidth | number | 1280 | No | The width of the window in SSR. |
initialHeight | number | 720 | No | The height of the window in SSR. |
Callbacks
Prop | Type | Default | Required? | Description |
---|
onRender | (startIndex: number, stopIndex: number, items: any[]) => void | undefined | No | This callback is invoked any time the items rendered in the grid 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 |
Methods
When a ref
is provided to this component, you'll have access to the following
imperative methods:
Method | Type | Description |
---|
clearPositions | () => void | Invoking this method will create a new position cache, clearing all previous stored position values. This is useful if you want the component to reflow when adding new items to the items array, however the best way to trigger a reflow is setting a different unique key prop on the <Masonry> component each time that happens. |
<FreeMasonry>
This is a bare bones masonry grid without useWindowScroller()
and useContainerRect()
hooks doing any magic. It accepts all of the props from <Masonry>
except initialWidth
and initialHeight
.
Additional props
Prop | Type | Default | Required? | Description |
---|
width | number | undefined | Yes | This sets the width of the grid. |
height | number | undefined | Yes | This is the height of the grid's window. If you're rendering <FreeMasonry> inside of a scrollable div for example, this would be the height of that div. |
scrollTop | number | undefined | Yes | The scroll position of the window <FreeMasonry> is rendering inside. Either the window object scroll position or the scroll position of say, a scrollable div you're rendering inside. |
isScrolling | boolean | false | No | When this value is true , pointer-events: none; and will-change: contents, height; styles are applied to the grid to maximize scroll performance. |
containerRef | ((element: HTMLElement) => void) | React.MutableRefObject<HTMLElement | null> | undefined | No | Sets a ref prop on the grid container. |
<List>
This is a single-column <Masonry>
component. It accepts all of the properties defined in [<Masonry>
],
except columnGutter
, columnWidth
, and columnCount
.
Additional props
Prop | Type | Default | Required? | Description |
---|
rowGutter | number | 0 | No | This sets the amount of vertical space in pixels between rendered list items. |
useInfiniteLoader()
A React hook for seamlessly adding infinite scrolling behavior to <Masonry>
and
<List>
components.
import {Masonry, useInfiniteLoader} from 'masonic'
import memoize from 'trie-memoize'
const fetchMoreItems = memoize(
[{}, {}, {}],
(startIndex, stopIndex, currentItems) =>
fetch(
`/api/get-more?after=${startIndex}&limit=${startIndex + stopIndex}`
).then(items => {
})
)
const InfiniteMasonry = props => {
const maybeLoadMore = useInfiniteLoader(fetchMoreItems)
const items = useItemsFromInfiniteLoader()
return <Masonry {...props} items={items} onRender={maybeLoadMore} />
}
Arguments
Argument | Type | Description |
---|
loadMoreItems | (startIndex: number, stopIndex: number, items: any[]) => any | This callback will be invoked when more items must be loaded. It may be called multiple times in reaction to a single scroll event. As such, you are expected to memoize/track whether or not you've already received the startIndex , stopIndex , items values to prevent loading data more than once. |
options | InfiniteLoaderOptions | Configuration object for your loader, see InfiniteLoaderOptions below. |
InfiniteLoaderOptions
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. Return true if the item has already been loaded and false if not. |
minimumBatchSize | number | 16 | |
threshold | number | 16 | The default value of 16 means that data will start loading when a user scrolls within 16 items of the end of your items prop 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 (e.g., the default value). |
useWindowScroller()
A hook used for measuring the size of the browser window, whether or not the window is currently being scrolled,
and the window's scroll position. These values are used when calculating the number of rows to render and determining
when we should disable pointer events on the masonry container to maximize scroll performance.
import React from 'react'
import {FreeMasonry, useWindowScroller, useContainerRect} from 'masonic'
const MyCustomMasonry = props => {
const {width, height, scrollY, isScrolling} = useWindowScroller(),
[rect, containerRef] = useContainerRect(width, height)
return React.createElement(
FreeMasonry,
Object.assign(
{
width: rect.width,
height,
scrollTop: Math.max(0, scrollY - (rect.top + scrollY)),
isScrolling,
containerRef,
},
props
)
)
}
Arguments
Argument | Type | Description |
---|
initialWidth | number | The width of the window when render on the server side. This has no effect client side. |
initialHeight | number | The height of the window when render on the server side. This has no effect client side. |
options | WindowScrollerOptions | A configuration object for the hook. See WindowScrollerOptions below. |
WindowScrollerOptions
interface WindowScrollerOptions {
size?: {
wait?: number
}
scroll?: {
fps?: number
}
}
Returns WindowScrollerResult
interface WindowScrollerResult {
width: number
height: number
scrollY: number
isScrolling: boolean
}
useContainerRect()
A hook used for measuring and tracking the width of the masonry component's container, as well as its distance from
the top of your document. These values are necessary to correctly calculate the number/width of columns to render, as well as the number of rows to render.
import React from 'react'
import {FreeMasonry, useWindowScroller, useContainerRect} from 'masonic'
const MyCustomMasonry = props => {
const {width, height, scrollY, isScrolling} = useWindowScroller(),
[rect, containerRef] = useContainerRect(width, height)
return React.createElement(
FreeMasonry,
Object.assign(
{
width: rect.width,
height,
scrollTop: Math.max(0, scrollY - (rect.top + scrollY)),
isScrolling,
containerRef,
},
props
)
)
}
Arguments
Argument | Type | Description |
---|
windowWidth | number | The width of the window. Used for updating the ContainerRect when the window's width changes. |
windowHeight | number | The height of the window. Used for updating the ContainerRect when the window's height changes. |
Returns [ContainerRect, (element: HTMLElement) => void]
ContainerRect
Property | Type | Description |
---|
top | number | The top value from element.getBoundingClientRect() |
width | number | The width value from element.getBoundingClientRect() |
Differences from react-virtualized/Masonry
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 will auto-calculate the number of columns to render based
upon the defined columnWidth
property. The column count will update
any time it changes.
-
The implementation for updating cell positions and sizes is also 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
is triggered.
-
The API is a complete rewrite and because of much of what is mentioned
above, is much easier to use in my opinion.
LICENSE
MIT