Context preservation on page destruction in React Native and Web using RecyclerListView

Talha Naqvi
5 min readSep 24, 2017

--

I think we’re all familiar with the situation where we prevent pages from being destroyed in the background just to prevent loss of scroll positions. A page might have multiple scrollable lists all of which will get reset in case the page needs to be redrawn. In this post I’ll write about how RecyclerListView prevents that from happening and makes it super easy.

RecyclerListView is a high performance listview, built at Flipkart, designed to specifically solve performance related issues for both react native and web. You can read about why should you be using it today here: https://medium.com/@naqvitalha/recyclerlistview-high-performance-listview-for-react-native-and-web-e368d6f0d7ef

It would be best if you go through the samples and build a basic understanding before proceeding.

Coming back to the post, to solve the problem of context preservation RecyclerListView uses something called as ContextProvider. ContextProvider is a simple class that you inherit from and then pass on your custom implementation to RecyclerListView. It looks like this:

export default class ContextProvider {
//Should be of string type unique in global scope
getUniqueKey() {

}

//Let recycler view save a value
save(key, value) {

}

//Get value for a key
get(key) {

}

//Remove key value pair
remove(key) {

}
}

It’s simple to implement, all you need to do is provide a way to store key/value pairs and provide a unique key to enable RecyclerListView to uniquely identify itself.

ContextProvider takes care of all possible scroll scenarios where height and widths of items are deterministic or non deterministic. It works even if you have no idea about the height and widths of items you’re about to render. It caches layout information so that it can instantly redraw the list from the right position.

Let’s take an example of a page with multiple horizontal RecyclerListViews that the user can independently scroll. There’ll be a toggle button on top which will destroy the whole page and then on next tap will create it again.

Let’s start, so there will be a parent RecyclerListView which will render all other lists and every list item will again be a RecyclerListView. We’ll begin by creating LayoutProviders for these lists and ContextProvider for the parent, we’ll do this in the constructor:

//Layout provider for parent list, exact dimensions
this._parentRLVLayoutProvider = new LayoutProvider(
index => {
return ViewTypes.SIMPLE_ROW;
},
(type, dim) => {
dim.height = 100;
dim.width = width;
}
);

//Layout provider for children lists, estimated dimensions
this._childRLVLayoutProvider = new LayoutProvider(
index => {
return ViewTypes.SIMPLE_ROW;
},
(type, dim) => {
dim.height = 100;
dim.width = 100;
}
);

this._parentContextProvider = new ContextHelper('PARENT');
this.state = {
isViewMounted: true,
parentRLVDataProvider: new DataProvider((r1, r2) => {
return r1 !== r2;
}).cloneWithRows(this._parentArr)
};

You must have noticed ContextHelper, it is our simplistic implementation of a context provider. This is what it looks like:

import {ContextProvider} from 'recyclerlistview';

export default class ContextHelper extends ContextProvider {
constructor(uniqueKey) {
super();
this._contextStore = {};
this._uniqueKey = uniqueKey;
}

getUniqueKey() {
return this._uniqueKey;
};

save(key, value) {
this._contextStore[key] = value;
}

get(key) {
return this._contextStore[key];
}

remove(key) {
delete this._contextStore[key];
}
}

ContextHelper simply uses an in memory map to provide context storage to RecyclerListView. You can do lot of improvisations here like using permanent storage depending upon your use case. By letting developers write their own ContextProviders RecyclerListView enables a lot of flexibility in terms of preserving context during page navigations or app launches.

Moving on, let’s generate some data for internal lists. We’ll write _generateData() method, this can be called in the constructor itself.

_generateData() {
//There will be 10 lists in total
this._parentArr = new Array(10);

//Each list will use these fruit names as data. Text being of
//variable lengths will result in non deterministic widths
this._childArr = [
'Apple',
'Banana',
'Custard Apple',
'Dragon Fruit',
'Egg Fruit',
'Finger Lime',
'Grapes',
'Honeydew Melon',
'Indonesian Lime',
'Jackfruit',
'Kiwi',
'Lychee',
'Mango',
'Navel Orange',
'Oranges',
'Pomegranate',
'Queen Anne Cherry',
'Raspberries',
'Strawberries',
'Tomato',
'Ugni',
'Vanilla Bean',
'Watermelon',
'Ximenia caffra fruit',
'Yellow Passion Fruit',
'Zuchinni'
];

//Every list in parent has its own data provider and context
//provider which we create here, to know more about this
//check samples of RecyclerListView
for (let i = 0; i < this._parentArr.length; i++) {
this._parentArr[i] = {
dataProvider: new DataProvider((r1, r2) => {
return r1 !== r2;
}).cloneWithRows(this._childArr),

//Proving unique key to context provider, using index as
//unique key here. You can choose your own, this
//should be unique in global scope ideally.
contextProvider: new ContextHelper(i + '')
};
}
}

This is really all you need. ContextHelper is as simple as it gets. Post this it’s same as rendering a regular RecyclerListView. The size of the list hardly impacts performance since RecyclerListView leverages progressive rendering i.e, renders only what needs to be shown. Let’s write rowRenderers for both child lists and the parent:

//Render internal lists with fruit names, uses non deterministic
//rendering
_parentRowRenderer = (type, data) => {
return (
<RecyclerListView
style={{flex: 1}}
showsHorizontalScrollIndicator={false}
isHorizontal={true}
dataProvider={data.dataProvider}
contextProvider={data.contextProvider}
layoutProvider={this._childRLVLayoutProvider}
forceNonDeterministicRendering={true}
rowRenderer={this._childRowRenderer}
/>
);
};

_childRowRenderer = (type, data) => {
return (
<View style={styles.textContainer}>
<Text style={styles.text}>{data}</Text>
</View>
);
};

And finally the render() method:

//Parent is also a RecyclerListView with its own context provider
//which means even the vertical scroll position will be preserved
render() {
return (
<View style={styles.container}>
<TouchableHighlight style={styles.toggleButton} onPress={this._onToggle}>
<Text style={{color: 'white'}}>Toggle</Text>
</TouchableHighlight>
{this.state.isViewMounted
? <RecyclerListView
style={{flex: 1}}
showsVerticalScrollIndicator={false}
contextProvider={this._parentContextProvider}
layoutProvider={this._parentRLVLayoutProvider}
dataProvider={this.state.parentRLVDataProvider}
rowRenderer={this._parentRowRenderer}
/>
: <Text style={styles.indicatorText}>Click on toggle to mount lists again</Text>}
</View>
);
}

We’ll also maintain isViewMounted boolean in state of the component in order to mount/unmount view for the purpose of the demo. We’ll use the following method to toggle:

_onToggle = () => {
this.setState({
isViewMounted: !this.state.isViewMounted
});
};

And that’s all you need. RecyclerListView makes it super simple to restore context, all you need is a simple ContextProvider on top of what you’ve already done. All of this works on React Native as well as web.

Let us know what you think of this, you can find the full source and play around with the demo at: https://github.com/naqvitalha/recyclerlistview-context-preservation-demo

Contact Us!

Please open issues for any bugs that you encounter. You can reach out to me on twitter @naqvitalha or, write to cross-platform@flipkart.com for any questions that you might have.

--

--