React Native Masonry with Infinite Scrolling

Luis Miguel Rojas Aguilera
6 min readJun 3, 2020
Masonry Component final look

Isn’t it beautiful ? A masonry always looks appealing, whether in real life or in digital user interfaces. Putting a masonry up using React Native doesn’t have to be as hard as it is in real life. This publication presents a step by step on how to construct a masonry for a mobile app, using react native.

Preliminary thoughts

Initially you might think, this can be achieved by setting flexWrap=”wrap” to a FlatList, then you just pass a set of images using data and renderItem props and styling takes care of element’s positioning. However flexWrap is not supported on FlatLists, if you google that you should find a github issue with some React maintainer saying:

React maintainer on FlatList’s flexWrap support

Yeah we are essentially constructing a grid with two columns, why don’t we use numColumns={2} then ? Because we want to render different height items and as per documentation:

Well that lets us with ScrollView, yeah I know you wanted FlatList’s data loading efficiency ( we want infinite scrolling after all ). Lets just use ScrollView by now and we work on data loading efficiency in the future.

OK, so it’s going to be a ScrollView with flexWrap styling then? well, the thing with that is, since we are building an infinite scrolling masonry, when data is updated, wrapping would rearrange some images, modifying its initial position and leading to a poor user experience. Let me show you my layout idea:

Masonry layout sketch

That way new cards loaded while scrolling are stacked at the bottom of each column. We just need to load all data, split it into two and distribute each split into those columns.

For sake of simplicity, instead of complex cards let’s render images. Those can be pulled from Lorem Picsum’s API. For infinite scrolling we can put an event handler for ScrollView’s onScroll that will add new images/cards to masonry at a given scrolling threshold (e.g when scroll is getting to last rendered cards).

Now lets get to coding.

Masonry Layout

OK let’s break simple, dropping a few components towards an initial layout.

// Masonry.js
import React, { Component } from 'react';
import { View, ScrollView } from 'react-native';
export default Masonry extends Component {
...
render(){
return
(<ScrollView>
<View>
// Column A
<View>
// Render first group of cards here
</View>
// Column B
<View>
// Render second group of cards here
</View>
</View>
</ScrollView>);
}
...

Now we want Column A and Column B to be one beside another, thus we are using flexDirection=’row’ on its parent component’s style. Also we need to set root View component’s width equals to phone’s width.

// Masonry.jsimport React, { Component } from 'react';
import { View, ScrollView, StyleSheet, Dimensions } from 'react-native';
export default Masonry extends Component {

constructor(props){
super(props);
this.vpWidth = Dimensions.get('window').width;
this.styles = StyleSheet.create({
container: {
width: this.vpWidth,
flexDirection: 'row'
}
});
}
render(){
return
(<ScrollView>
<View
style={this.styles.container}
>
// Column A
<View>
// Render first group of cards here
</View>
// Column B
<View>
// Render second group of cards here
</View>
</View>
</ScrollView>);
}
...

There we have an initial layout. Now we need to add card’s rendering/loading dynamic for an infinite scrolling.

Data Loading

Cards on our masonry will most likely use some data to render contents inside of it like some text, prices, image’s uri, etc. Also cards should re-render in case data changes (like it will do when new cards appear on infinite scrolling). Lets express that dynamic with react states.

// Masonry.jsimport React, { Component } from 'react';
import { View, ScrollView, StyleSheet, Dimensions } from 'react-native';
export default Masonry extends Component {

constructor(props){
super(props);
... this.pageSize = this.props.pageSize | 50; ...
this.state = {
data: []
}
...
}
... generateData(){ const data = this.props.itemsProvider(this.pageSize); this.setState({
data: [...this.state.data, ...data]
});
} componentDidMount(){ this.generateData(); } ...

render(){
const data = this.state.data;
...
// Column A
<View>
// Render first group of cards here
{
data.length ?
data.slice(0, data.length / 2).map(
(di, i) => {
return this.props.renderItem(di, i)
}) : (<></>)
}
</View>
// Column B
<View>
// Render second group of cards here
{
data.length ?
data.slice(data.length / 2, data.length)
.map((di, i) => {
return this.props.renderItem(di, i)
}) : (<></>)
}
</View>
...
}
...

Basically we are defining card’s data as a state entry. That data is initially populated first time masonry is mounted and most likely will be populated again when ScrollView’s scroll offset is near to last cards rendered, that’s why we created a function to trigger data population and state update (generateData) we can use for that matter.

Since we want a decoupled and configurable masonry component, we need to setup a way for cards that will be rendered inside ScrollView as well as data used by such cards, to be provided from outside the masonry (i.e configured by the component the masonry is wrapped on). That can be accomplished using props. That’s why we added renderItem and itemsProvider props as you can see in the code above.

Infinite scrolling

When ScrollView’s scroll offset is getting close to last rendered cards, new data should be gathered and new cards rendered for that data. For that matter, lets set a handler on ScrollViews’s onScroll event listener and a way to detect if its time to trigger data fetching.

// Masonry.jsimport { View, ScrollView, StyleSheet, Dimensions } from 'react-native';export default Masonry extends Component {

constructor(props){
...
this.scrollViewHeight = 0;
this.vpHeight = Dimensions.get('window').height;
...
} ... handleScroll(e){ const {y} = e.nativeEvent.contentOffset;
const height = this.scrollViewHeight;
const lastScreenOffset = height - this.vpHeight * 3;
if(y >= lastScreenOffset){
this.generateData();
}
} logScrollViewSize(width, height){ this.scrollViewHeight = height;
}

...
render(){
...
return (
<ScrollView
onScroll={this.handleScroll}
onContentSizeChange={this.logScrollViewSize}
>
...
}
}

Note in code above how we set handlers for ScrollView’s onScroll and onContentSizeChange to keep track of ScrollView’s height and trigger data fetching once scroll offset is close to that value. Actually we are fetching data when scroll offset is three times phone’s screen height from ScrollView’s bottom so cards are already rendered when scrolling gets there.

Using the Masonry Component

Now we can use our newly created Masonry Component in any place, as long as we pass a data provider and render prop function.

// App.jsimport Masonry from ./Masonryexport default function App() {
return (
<SafeAreaView style={styles.container}>
<Masonry
itemsProvider={dataItemProvider}
renderItem={Item}
pageSize={10}
/>
</SafeAreaView>
);
}
// Individual card's provider to be rendered by Masonry
function Item(dataItem, key){
return (
<View
key={key}
style={{
...styles.card,
height: dataItem.height
}}
>
<Image
style={styles.img}
source={{uri: dataItem.image_url}}
/>
</View>
);
}
// Card's data provider
function dataItemProvider(pageSize=10){

return [...Array(pageSize).keys()].map((i) => {
return {
image_url: `https://i.picsum.photos/id/${parseInt(Math.random() * 200)}/300/400.jpg`,
height: parseInt(Math.max(0.3, Math.random()) * vpWidth),
key:i
};
});
}
...

In code above, look how we are passing card’s rendering and data gathering functions as Masonry’s props. This kind of decoupling and separation of concerns allows the Masonry to be used with several custom configurations.

In this case we are using images taken from Lorem Picsum site, as cards. Also notice how random card’s height values are generated so it look more appealing on the Masonry.

Conclusion

In this post we have created a decoupled configurable masonry component with infinite scrolling. Also some concepts in Java Script, React Native and good coding practices were approached. Now our Masonry component can be used in any app, next step: turn it into a package.

Now you can install this masonry with npm or yarn (yarn add react-native-infinite-masonry).

For a working example check at my github: https://github.com/roj4s/

--

--

Luis Miguel Rojas Aguilera

Versatile Software Engineer with 7+ years dealing with Web, AI, Machine Learning and QA. MsC in Computer Sciences.