Image Pipeline with React Native ListView
In mobile apps, scrolling through a list of images is a very common use case for which users have high expectations. In a previous story, I shared formulas that I used in order to cache images with RN. In this story, I would like to share how ListView can be used to optimize image processing when scrolling through images.
Consider a list of entries like an Instagram feed. We fetch a page from the server. At first, when the user scrolls towards the end of the list, we fetch more entries from the server. Secondly, when an entry enters the viewport, we trigger the image cache to display the image. Finally, when the entry goes outside the viewport, and if the image hasn’t been cached yet, we would like to cancel the request to the image URI in order to prioritize requests for the visible entries.
In the example below, we fetch entries from a firebase backend by pages of ten. When we reach the end of the list, we fetch a new page by using the onEndReached event.
export default class Feed extends Component<{}, void> {
currentPage = 1;
pageSize = 10;
feed: ListViewDataSource =
new ListView.DataSource({ rowHasChanged: (r1, r2) => r1.id !== r2.id });
loadPage() {
firebase.database().ref("/feed")
.limitToFirst(this.pageSize * this.currentPage)
.once("value").then(snapshot => {
this.currentPage++;
this.feed = this.feed.cloneWithRows(snapshot.val());
});
}
componentWillMount() { this.loadFeed(); }
render() {
const {store} = this.props;
return <ListView
dataSource={store.feed}
renderRow={row => <View><CachedImage source={{ uri: row.uri }} /></View> }
onEndReached={() => this.loadFeed()}
/>;
}
}
Now we can use onChangeVisibleRows() to only mount images when they are visible. We create a row component with a visible attribute. We use the ref() callback to keep a map up to date of each of the mounted rows. In onChangeVisibleRows(), we update the visible property for each row that becomes visible or invisible.
export default class Feed extends Component<{}, void> {
//Keep a reference of all mounted Rows
rows: { [id: string ]: any } = {};
render() {
return <ListView
dataSource={store.feed}
// We use ref to keep the reference of all mounted rows
// The visible property will be set to true in onChangeVisibleRows()
renderRow={row => <Row ref={el => this.rows[row.id] = el} visible={false} row={row} />}
onEndReached={() => this.loadFeed()}
onChangeVisibleRows={(visibleRows, changedRows) => {
_.forEach(changedRows["s1"], (visible, index) => {
// We get the reference of the mounted row
//and set its property to true
const id = this.feed.getRowData(0, index).id;
this.rows[id].props.visible = visible;
});
}} />;
}
}
In the Row component, we can use the visible property to mount/unmount the image according to its visibility. We use the react-native-img-cache package for this.
export default class Row extends Component<RowProps, void> {
render() {
const {visible, row} = this.props;
return <View>
{visible && <CachedImage source={{ uri: row.pictureURI }} />}
</View>;
}
}
In the last step, we cancel the HTTP request to download the image if the row becomes invisible. react-native-img-cache relies on react-native-fetch-blob which provides a way to both download image without sending it over the JS bridge and to cancel an HTTP request.
if(!visible) {
ImageCache.get().cancel(row.uri);
}
Do you find this guide useful? I’m looking forward to reading your feedback.