Efficient React ListView for Realm collections

This component is fully backwards compatible with the original React ListView, but is compatible with Realm Results and List objects to use their snapshot functionality along with more efficiently checking if each row should update.
This commit is contained in:
Scott Kyle 2016-01-11 15:11:15 -08:00
parent 58ef90dc53
commit eb4ac0000b
4 changed files with 205 additions and 1 deletions

View File

@ -2,7 +2,7 @@
"env": {
"commonjs": true,
"browser": true,
"es6": true,
"es6": true
},
"ecmaFeatures": {
"forOf": false

21
react-native/.eslintrc Normal file
View File

@ -0,0 +1,21 @@
{
"env": {
"commonjs": true,
"es6": true
},
"ecmaFeatures": {
"forOf": false,
"jsx": true
},
"plugins": [
"react"
],
"rules": {
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-undef": 2,
"react/jsx-uses-react": 2,
"react/no-direct-mutation-state": 1,
"react/prefer-es6-class": 1,
"react/react-in-jsx-scope": 2
}
}

7
react-native/index.js vendored Normal file
View File

@ -0,0 +1,7 @@
/* Copyright 2015 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/
'use strict';
exports.ListView = require('./listview');

176
react-native/listview.js vendored Normal file
View File

@ -0,0 +1,176 @@
/* Copyright 2015 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/
'use strict';
const React = require('react-native');
function hashObjects(array) {
let hash = Object.create(null);
for (let i = 0, len = array.length; i < len; i++) {
hash[array[i]] = true;
}
return hash;
}
class ListViewDataSource extends React.ListView.DataSource {
cloneWithRowsAndSections(inputData, sectionIds, rowIds) {
let data = {};
for (let sectionId in inputData) {
let items = inputData[sectionId];
let copy;
// Realm Results and List objects have a snapshot() method.
if (typeof items.snapshot == 'function') {
copy = items.snapshot();
} else if (Array.isArray(items)) {
copy = items.slice();
} else {
copy = Object.assign({}, items);
}
data[sectionId] = copy;
}
if (!sectionIds) {
sectionIds = Object.keys(data);
}
if (!rowIds) {
rowIds = sectionIds.map((sectionId) => {
let items = data[sectionId];
if (typeof items.snapshot != 'function') {
return Object.keys(items);
}
// Efficiently get the keys of the Realm collection, since they're never sparse.
let count = items.length;
let indexes = new Array(count);
for (let i = 0; i < count; i++) {
indexes[i] = i;
}
return indexes;
});
}
// Copy this object with the same parameters initially passed into the constructor.
let newSource = new this.constructor({
getRowData: this._getRowData,
getSectionHeaderData: this._getSectionHeaderData,
rowHasChanged: this._rowHasChanged,
sectionHeaderHasChanged: this._sectionHeaderHasChanged,
});
newSource._cachedRowCount = rowIds.reduce((n, a) => n + a.length, 0);
newSource._dataBlob = data;
newSource.sectionIdentities = sectionIds;
newSource.rowIdentities = rowIds;
let prevSectionIds = this.sectionIdentities;
let prevRowIds = this.rowIdentities;
let prevRowHash = {};
for (let i = 0, len = prevRowIds.length; i < len; i++) {
prevRowHash[prevSectionIds[i]] = hashObjects(prevRowIds[i]);
}
// These properties allow lazily calculating if rows and section headers should update.
newSource._prevDataBlob = this._dataBlob;
newSource._prevSectionHash = hashObjects(prevSectionIds);
newSource._prevRowHash = prevRowHash;
return newSource;
}
getRowData() {
// The React.ListView calls this for *every* item during each render, which is quite
// premature since this can be mildly expensive and memory inefficient since it keeps
// the result of this alive through a bound renderRow function.
return null;
}
getRow(sectionId, rowId) {
// This new method is provided as a convenience for those wishing to be memory efficient.
return this._getRowData(this._dataBlob, sectionId, rowId);
}
sectionHeaderShouldUpdate(sectionIndex) {
let dirtySections = this._dirtySections;
let dirty;
if ((dirty = dirtySections[sectionIndex]) != null) {
// This was already calculated before.
return dirty;
}
let sectionId = this.sectionIdentities[sectionIndex];
let sectionHeaderHasChanged = this._sectionHeaderHasChanged;
if (this._prevSectionHash[sectionId] && sectionHeaderHasChanged) {
dirty = sectionHeaderHasChanged(
this._getSectionHeaderData(this._prevDataBlob, sectionId),
this._getSectionHeaderData(this._dataBlob, sectionId)
);
}
// Unless it's explicitly *not* dirty, then this section header should update.
return (dirtySections[sectionIndex] = dirty !== false);
}
rowShouldUpdate(sectionIndex, rowIndex) {
let dirtyRows = this._dirtyRows[sectionIndex];
let dirty;
if (!dirtyRows) {
dirtyRows = this._dirtyRows[sectionIndex] = [];
} else if ((dirty = dirtyRows[rowIndex]) != null) {
// This was already calculated before.
return dirty;
}
let sectionId = this.sectionIdentities[sectionIndex];
if (this._prevSectionHash[sectionId]) {
let rowId = this.rowIdentities[sectionIndex][rowIndex];
if (this._prevRowHash[sectionId][rowId]) {
let prevItem = this._getRowData(this._prevDataBlob, sectionId, rowId);
if (prevItem) {
let item = this._getRowData(this._dataBlob, sectionId, rowId);
if (item) {
dirty = this._rowHasChanged(prevItem, item);
}
}
}
}
// Unless it's explicitly *not* dirty, then this row should update.
return (dirtyRows[rowIndex] = dirty !== false);
}
}
class ListView extends React.Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
render() {
return <React.ListView {...this.props} renderRow={this.renderRow} />;
}
renderRow(_, sectionId, rowId, ...args) {
let props = this.props;
let item = props.dataSource.getRow(sectionId, rowId);
// The item could be null because our data is a snapshot and it was deleted.
return item ? props.renderRow(item, sectionId, rowId, ...args) : null;
}
}
ListView.propTypes = {
dataSource: React.PropTypes.instanceOf(ListViewDataSource).isRequired,
renderRow: React.PropTypes.func.isRequired,
};
ListView.DataSource = ListViewDataSource;
module.exports = ListView;