From 26e2e76e962d64dc1dfea49dfbef3beb173864f5 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Sat, 2 Apr 2016 12:37:19 -0700 Subject: [PATCH 1/9] solutions for infiniteScrolling onEndReached firing when it shouldn't this ended up being a way bigger trek than I anticipated. Basically, i found a bunch of edge cases which cause onEndReached to fire when it shouldn't. I used the responder system and datetime flags to figure out what the user's likely intent is in as many scenarios as I encountered. My app using this is now squeaky clean--no extra async calls to my server. Detailed notes are the comments. There is also a few new features and refactorings. This isn't tested for Android, only iOS. But if Android infinite scroll was already working, we just need to make sure this doesn't conflict. --- GiftedListView.js | 157 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 128 insertions(+), 29 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 149a3bd..51f1c07 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -9,6 +9,7 @@ var { View, Text, RefreshControl, + ScrollView, } = React; @@ -36,6 +37,8 @@ var GiftedListView = React.createClass({ return { customStyles: {}, initialListSize: 10, + onEndReachedThreshold: 100, + onEndReachedEventThrottle: 1000, firstLoader: true, pagination: true, refreshable: true, @@ -49,6 +52,7 @@ var GiftedListView = React.createClass({ sectionHeaderView: null, scrollEnabled: true, withSections: false, + autoPaginate: false, onFetch(page, callback, options) { callback([]); }, paginationFetchingView: null, @@ -62,6 +66,8 @@ var GiftedListView = React.createClass({ propTypes: { customStyles: React.PropTypes.object, initialListSize: React.PropTypes.number, + onEndReachedThreshold: React.PropTypes.number, + onEndReachedEventThrottle: React.PropTypes.number, firstLoader: React.PropTypes.bool, pagination: React.PropTypes.bool, refreshable: React.PropTypes.bool, @@ -75,6 +81,7 @@ var GiftedListView = React.createClass({ sectionHeaderView: React.PropTypes.func, scrollEnabled: React.PropTypes.bool, withSections: React.PropTypes.bool, + autoPaginate: React.PropTypes.bool, onFetch: React.PropTypes.func, paginationFetchingView: React.PropTypes.func, @@ -197,15 +204,40 @@ var GiftedListView = React.createClass({ }, componentDidMount() { - this.props.onFetch(this._getPage(), this._postRefresh, {firstLoad: true}); + this._fetch(this._getPage(), {firstLoad: true}); + + //imperative OOP state utilized since onEndReached is imperatively called. So why waste cycles on rendering, which + //can cause loss of frames in animation. + this.lastGrantAt = this.lastReleaseAt = this.lastEndReachedAt = this.lastManualRefreshAt = this.lastPaginateUpdateAt = new Date; }, setNativeProps(props) { this.refs.listview.setNativeProps(props); }, - _refresh() { - this._onRefresh({external: true}); + //The refactoring was done solely so we can pass `beforeOptions` along + //and insure such things as `lastManualRefreshAt` are passed to client code and back to our `_updateRows` method. + //But I think this could be useful for any data we want to pass to developers and guarantee comes back to us. + _fetch(page, beforeOptions, postCallback) { + postCallback = postCallback || this._postRefresh; + + this.props.onFetch(page, (rows, options) => { + postCallback(rows, Object.assign(beforeOptions, options)); + }, beforeOptions); + }, + + scrollTo(config) { + this.refs.listview.scrollTo(config); + }, + _refresh(options) { + this.lastManualRefreshAt = new Date; //can trigger scrollview to push past endReached threshold if you are already scrolled down when you call this + + this._onRefresh(Object.assign({ + external: true, + mustSetLastManualRefreshAt: true, //we pass it along, so when the rows are updated we know to store the date as well + }, options)); + + if(options.scrollToTop) this.scrollTo({y: -80}); //if you manually refresh the list, you often want to go to the top again, such as when filtering }, _onRefresh(options = {}) { @@ -214,7 +246,7 @@ var GiftedListView = React.createClass({ isRefreshing: true, }); this._setPage(1); - this.props.onFetch(this._getPage(), this._postRefresh, options); + this._fetch(this._getPage(), options); } }, @@ -224,58 +256,107 @@ var GiftedListView = React.createClass({ } }, + onEndReached() { + //firstLoadCompleteAte prevents any onEndReached firings in initial rendering. There is usuallyl 2 such firings you don't want. + if(!this.firstLoadCompleteAt || new Date - this.firstLoadCompleteAt < 1000) return; + + //lastPaginateUpdateAt solves the issue where paginationView's disappearing trigger onEndReached. + //This happens when you're near the end of the page and the dissapperance of the pagination view + //triggers onEndReached. The timing is so small so as not to disrupt other regular scrolling behavior. + if(new Date - this.lastPaginateUpdateAt < 300) return; + + //lastManualRefreshAt handles the case where you call _refresh(), which if you do while the page is near the end + //will trigger onEndReached even though you just moments ago manually refreshed. + if(new Date - this.lastManualRefreshAt < 300) return; + + //Here's the bread and butter of strong event firing management in regards to when the user in fact does want lots of pagination refreshes: + + //The base case is simply lastEndReachedAt, which very easily can fire, so we want to block that while still allowing for + //fast scrolling. If you scroll to the end of the page again within one second (fast scrolling), it will know you want more based + //on lastReleasedAt (you will have to have released multiple times to scroll fast). lastGrantAt is for if you have short rows + //and/or a low # of rows per page and you're able to move to the end without even releasing your finger. + if(new Date - this.lastEndReachedAt < (this.props.onEndReachedEventThrottle || 1000)) { + if(new Date - this.lastGrantAt < 3000) return; //we can likely lower this number, + if(new Date - this.lastReleaseAt < 3000) return; //or make it configurable via props, but I think making it configurable will be unwanted added complexity for client developers + } + + this.lastEndReachedAt = new Date; + + + if (this.props.autoPaginate) { + this._onPaginate(); + } + if (this.props.onEndReached) { + this.props.onEndReached(); + } + }, + + + onResponderGrant() { + this.lastGrantAt = new Date; + }, + onResponderRelease() { + this.lastReleaseAt = new Date; + }, _onPaginate() { - if(this.state.paginationStatus==='allLoaded'){ - return null - }else { - this.setState({ - paginationStatus: 'fetching', - }); - this.props.onFetch(this._getPage() + 1, this._postPaginate, {}); + if (this.state.paginationStatus === 'firstLoad' || this.state.paginationStatus === 'waiting') { + this.setState({paginationStatus: 'fetching'}); + this._fetch(this._getPage() + 1, {}, this._postPaginate); } }, _postPaginate(rows = [], options = {}) { this._setPage(this._getPage() + 1); + var mergedRows = null; + if (this.props.withSections === true) { mergedRows = MergeRecursive(this._getRows(), rows); } else { mergedRows = this._getRows().concat(rows); } + + this.lastPaginateUpdateAt = new Date; + this._updateRows(mergedRows, options); }, + _updateRows(rows = [], options = {}) { + let state = { + isRefreshing: false, + paginationStatus: (options.allLoaded === true ? 'allLoaded' : 'waiting'), + }; + + if(options.mustSetLastManualRefreshAt) this.lastManualRefreshAt = new Date; + if (rows !== null) { this._setRows(rows); + if (this.props.withSections === true) { - this.setState({ - dataSource: this.state.dataSource.cloneWithRowsAndSections(rows), - isRefreshing: false, - paginationStatus: (options.allLoaded === true ? 'allLoaded' : 'waiting'), - }); + state.dataSource = this.state.dataSource.cloneWithRowsAndSections(rows); } else { - this.setState({ - dataSource: this.state.dataSource.cloneWithRows(rows), - isRefreshing: false, - paginationStatus: (options.allLoaded === true ? 'allLoaded' : 'waiting'), - }); + state.dataSource = this.state.dataSource.cloneWithRows(rows); } - } else { - this.setState({ - isRefreshing: false, - paginationStatus: (options.allLoaded === true ? 'allLoaded' : 'waiting'), - }); } + + this.setState(state); + + //this must be fired separately or iOS will call onEndReached 2-3 additional times as + //the ListView is filled. So instead we rely on React's rendering to cue this task + //until after the previous state is filled and the ListView rendered. After that, + //onEndReached callbacks will fire. See onEndReached() above. + if(!this.firstLoadCompleteAt) this.firstLoadCompleteAt = new Date; }, _renderPaginationView() { - if ((this.state.paginationStatus === 'fetching' && this.props.pagination === true) || (this.state.paginationStatus === 'firstLoad' && this.props.firstLoader === true)) { + let paginationEnabled = this.props.pagination === true || this.props.autoPaginate === true; + + if ((this.state.paginationStatus === 'fetching' && paginationEnabled) || (this.state.paginationStatus === 'firstLoad' && this.props.firstLoader === true)) { return this.paginationFetchingView(); - } else if (this.state.paginationStatus === 'waiting' && this.props.pagination === true && (this.props.withSections === true || this._getRows().length > 0)) { + } else if (this.state.paginationStatus === 'waiting' && this.props.pagination === true && (this.props.withSections === true || this._getRows().length > 0)) { //never show waiting for autoPaginate return this.paginationWaitingView(this._onPaginate); - } else if (this.state.paginationStatus === 'allLoaded' && this.props.pagination === true) { + } else if (this.state.paginationStatus === 'allLoaded' && paginationEnabled) { return this.paginationAllLoadedView(); } else if (this._getRows().length === 0) { return this.emptyView(this._onRefresh); @@ -312,6 +393,24 @@ var GiftedListView = React.createClass({ renderFooter={this._renderPaginationView} renderSeparator={this.renderSeparator} + onResponderGrant={this.onResponderGrant} + //onResponderMove={this.onResponderMove} + onResponderRelease={this.onResponderRelease} + //onMomentumScrollEnd={this.onMomentumScrollEnd} + + //check out this thread: https://github.com/facebook/react-native/issues/1410 + //and this stackoverflow post: http://stackoverflow.com/questions/33350556/how-to-get-onpress-event-from-scrollview-component-in-react-native + //basically onScrollAnimationEnd is incorrect (onMomentumScrollEnd is the right one) and all the native event callbacks + //are available, but no documented. Often times library developers do not want to build + //on top of such things. But my opinion in this case obviously is we should. The responderRelease code in call edonEndReached() is extremely stable and clear. + //I am willing to maintain this for a while, so in the rare case these become available, + //I will find something out. In all likelihood, only better APIs that are closer + //to our precise needs and do not require all this still will become available. When they do, I will implement them. But at the same timeout + //I find it unlikely that PanResponder methods that ScrollViews are based on will disappear, even if they remain undocumented for a long time. + + onEndReached={this.onEndReached} + onEndReachedThreshold={this.props.onEndReachedThreshold || 100} //new useful prop, yay! + automaticallyAdjustContentInsets={false} scrollEnabled={this.props.scrollEnabled} canCancelContentTouches={true} From 80b2c65a296c48d8aaa96b60d6e32fc22ead1e06 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Mon, 25 Apr 2016 00:28:44 -0700 Subject: [PATCH 2/9] declarative api via `componentWillReceive` props + `rows` prop this is primarily for usage with redux. the idea being that your `onFetch` prop will dispatch a redux action that ultimately will reduce the returns rows to the `rows` prop passed to GiftedListView, and when new rows are received, `componentWillReceiveProps` will serve the same purpose as the callback originally passed to `onFetch`. Therefore, onFetch doesn't ever need to call this callback (even though it will still receive it). When all rows are loaded just set the `allLoaded` prop to `true`. The only thing worth noting in the implementation is how the `beforeOptions` are kept in tack. Before they were passed to onFetch which ultimately passes them back, and possibly adds `allLoaded`, but now we store them on the instance at `this.beforeOptions`, resulting in calling code not having to worry about it at all, unless it wants to set `allLoaded` to true as described above. In addition, there's a `fetchOptions` prop which can be used to trigger a refresh, and then the calling of `onFetch` with the new params, which you can use to dispatch and make async queries which will ultimately reduce a new `rows` prop. The idea being that the interface used in its simplest most effective form consists of you just changing your `fetchOptions` :). --- GiftedListView.js | 60 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 51f1c07..7fa636c 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -60,6 +60,9 @@ var GiftedListView = React.createClass({ paginationWaitingView: null, emptyView: null, renderSeparator: null, + + rows: null, + fetchOptions: null, }; }, @@ -89,6 +92,9 @@ var GiftedListView = React.createClass({ paginationWaitingView: React.PropTypes.func, emptyView: React.PropTypes.func, renderSeparator: React.PropTypes.func, + + rows: React.PropTypes.array, + fetchOptions: React.PropTypes.object, }, _setPage(page) { this._page = page; }, @@ -215,20 +221,12 @@ var GiftedListView = React.createClass({ this.refs.listview.setNativeProps(props); }, - //The refactoring was done solely so we can pass `beforeOptions` along - //and insure such things as `lastManualRefreshAt` are passed to client code and back to our `_updateRows` method. - //But I think this could be useful for any data we want to pass to developers and guarantee comes back to us. - _fetch(page, beforeOptions, postCallback) { - postCallback = postCallback || this._postRefresh; - - this.props.onFetch(page, (rows, options) => { - postCallback(rows, Object.assign(beforeOptions, options)); - }, beforeOptions); - }, - scrollTo(config) { this.refs.listview.scrollTo(config); }, + refresh(options) { + return this._refresh(options); + }, _refresh(options) { this.lastManualRefreshAt = new Date; //can trigger scrollview to push past endReached threshold if you are already scrolled down when you call this @@ -250,6 +248,42 @@ var GiftedListView = React.createClass({ } }, + //The refactoring was done solely so we can pass `beforeOptions` along + //and insure such things as `lastManualRefreshAt` are passed to client code and back to our `_updateRows` method. + //But I think this could be useful for any data we want to pass to developers and guarantee comes back to us. + _fetch(page, beforeOptions, postCallback) { + postCallback = postCallback || this._postRefresh; + + Object.assign(beforeOptions, this.props.fetchOptions); //any fetch options will be passed along to `props.onFetch` in order to use for async queries + + this.beforeOptions = beforeOptions; //will be used by componentWillReceive props; parent components only need to provide rows + + this.props.onFetch(page, (rows, options) => { + postCallback(rows, Object.assign(beforeOptions, options)); + }, beforeOptions); + }, + + //Configure props for `onFetch`, `fetchOptions` and 'rows' to declaratively change the rows displayed. + //`onFetch` should now be used to dispatch a redux action, which reduces the `rows` prop :) + componentWillReceiveProps(nextProps) { + if(!this.props.rows) return; + + if(nextProps.rows !== this.props.rows) { + if(this.beforeOptions.paginatedFetch) { + this._postPaginate(rows, {...this.beforeOptions, allLoaded: nextProps.allLoaded}); + } + else this._postRefresh(rows, this.beforeOptions); + } + + //allow for declaratively refreshing simply by changing fetchOptions, + //which will then call `onFetch` and if done right will result in new `rows` props, i.e. the above code. + else if(nextProps.fetchOptions !== this.props.fetchOptions) { + this.refresh(nextProps.fetchOptions); + } + + this.beforeOptions = null; + }, + _postRefresh(rows = [], options = {}) { if (this.isMounted()) { this._updateRows(rows, options); @@ -301,7 +335,7 @@ var GiftedListView = React.createClass({ _onPaginate() { if (this.state.paginationStatus === 'firstLoad' || this.state.paginationStatus === 'waiting') { this.setState({paginationStatus: 'fetching'}); - this._fetch(this._getPage() + 1, {}, this._postPaginate); + this._fetch(this._getPage() + 1, {paginatedFetch: true}, this._postPaginate); } }, @@ -397,7 +431,7 @@ var GiftedListView = React.createClass({ //onResponderMove={this.onResponderMove} onResponderRelease={this.onResponderRelease} //onMomentumScrollEnd={this.onMomentumScrollEnd} - + //check out this thread: https://github.com/facebook/react-native/issues/1410 //and this stackoverflow post: http://stackoverflow.com/questions/33350556/how-to-get-onpress-event-from-scrollview-component-in-react-native //basically onScrollAnimationEnd is incorrect (onMomentumScrollEnd is the right one) and all the native event callbacks From aceaa0ebbf507e4bfb44d97225bdd8007b5e1162 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Wed, 4 May 2016 00:17:23 -0700 Subject: [PATCH 3/9] declarative fetching via componentWillReceiveProps --- GiftedListView.js | 98 ++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 7fa636c..f192f20 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -210,6 +210,7 @@ var GiftedListView = React.createClass({ }, componentDidMount() { + window.gifted = this; this._fetch(this._getPage(), {firstLoad: true}); //imperative OOP state utilized since onEndReached is imperatively called. So why waste cycles on rendering, which @@ -224,9 +225,12 @@ var GiftedListView = React.createClass({ scrollTo(config) { this.refs.listview.scrollTo(config); }, + + //open up `refresh` as a public API refresh(options) { return this._refresh(options); }, + _refresh(options) { this.lastManualRefreshAt = new Date; //can trigger scrollview to push past endReached threshold if you are already scrolled down when you call this @@ -235,7 +239,7 @@ var GiftedListView = React.createClass({ mustSetLastManualRefreshAt: true, //we pass it along, so when the rows are updated we know to store the date as well }, options)); - if(options.scrollToTop) this.scrollTo({y: -80}); //if you manually refresh the list, you often want to go to the top again, such as when filtering + //if(options.scrollToTop) this.scrollTo({y: -80}); //if you manually refresh the list, you often want to go to the top again, such as when filtering }, _onRefresh(options = {}) { @@ -244,7 +248,9 @@ var GiftedListView = React.createClass({ isRefreshing: true, }); this._setPage(1); - this._fetch(this._getPage(), options); + this.refreshedAt = new Date; + let page = this._getPage(); + this._fetch(page, options); } }, @@ -254,7 +260,7 @@ var GiftedListView = React.createClass({ _fetch(page, beforeOptions, postCallback) { postCallback = postCallback || this._postRefresh; - Object.assign(beforeOptions, this.props.fetchOptions); //any fetch options will be passed along to `props.onFetch` in order to use for async queries + Object.assign({}, beforeOptions, this.props.fetchOptions); //any fetch options will be passed along to `props.onFetch` in order to use for async queries this.beforeOptions = beforeOptions; //will be used by componentWillReceive props; parent components only need to provide rows @@ -265,23 +271,33 @@ var GiftedListView = React.createClass({ //Configure props for `onFetch`, `fetchOptions` and 'rows' to declaratively change the rows displayed. //`onFetch` should now be used to dispatch a redux action, which reduces the `rows` prop :) - componentWillReceiveProps(nextProps) { - if(!this.props.rows) return; + componentWillReceiveProps(nextProps, nextState) { + let rows = nextProps.rows; + + console.log("MAIN GIFTED", rows !== this.props.rows, JSON.stringify(nextProps.fetchOptions) !== JSON.stringify(this.props.fetchOptions)) + - if(nextProps.rows !== this.props.rows) { - if(this.beforeOptions.paginatedFetch) { + if(rows !== this.props.rows) { + if(this.beforeOptions && this.beforeOptions.paginatedFetch) { this._postPaginate(rows, {...this.beforeOptions, allLoaded: nextProps.allLoaded}); } - else this._postRefresh(rows, this.beforeOptions); + else { + let timeSinceRefresh = new Date - this.refreshedAt; //make sure at least 1 second goes by before hiding refresh control, + let delay = timeSinceRefresh > 1000 ? 0 : 1000 - timeSinceRefresh; //or otherwise it will stick to its open position + + setTimeout(() => { + this._postRefresh(rows, this.beforeOptions); + }, delay); + } } //allow for declaratively refreshing simply by changing fetchOptions, //which will then call `onFetch` and if done right will result in new `rows` props, i.e. the above code. - else if(nextProps.fetchOptions !== this.props.fetchOptions) { + else if(JSON.stringify(nextProps.fetchOptions) !== JSON.stringify(this.props.fetchOptions)) { this.refresh(nextProps.fetchOptions); } - this.beforeOptions = null; + this.beforeOptions = {}; }, _postRefresh(rows = [], options = {}) { @@ -290,6 +306,33 @@ var GiftedListView = React.createClass({ } }, + _updateRows(rows = [], options = {}) { + let state = { + isRefreshing: false, + paginationStatus: (options.allLoaded === true || rows.length === 0 ? 'allLoaded' : 'waiting'), + }; + + if(options.mustSetLastManualRefreshAt) this.lastManualRefreshAt = new Date; + + if (rows !== null) { + this._setRows(rows); + + if (this.props.withSections === true) { + state.dataSource = this.state.dataSource.cloneWithRowsAndSections(rows); + } else { + state.dataSource = this.state.dataSource.cloneWithRows(rows); + } + } + + this.setState(state, this.props.onRefresh); + + //this must be fired separately or iOS will call onEndReached 2-3 additional times as + //the ListView is filled. So instead we rely on React's rendering to cue this task + //until after the previous state is filled and the ListView rendered. After that, + //onEndReached callbacks will fire. See onEndReached() above. + if(!this.firstLoadCompleteAt) this.firstLoadCompleteAt = new Date; + }, + onEndReached() { //firstLoadCompleteAte prevents any onEndReached firings in initial rendering. There is usuallyl 2 such firings you don't want. if(!this.firstLoadCompleteAt || new Date - this.firstLoadCompleteAt < 1000) return; @@ -347,7 +390,12 @@ var GiftedListView = React.createClass({ if (this.props.withSections === true) { mergedRows = MergeRecursive(this._getRows(), rows); } else { - mergedRows = this._getRows().concat(rows); + if(this.props.dontConcat) { + mergedRows = rows; //because rows are already concatenated for use in a Redux store that needs access to all rows + } + else { + mergedRows = this._getRows().concat(rows); + } } this.lastPaginateUpdateAt = new Date; @@ -355,34 +403,6 @@ var GiftedListView = React.createClass({ this._updateRows(mergedRows, options); }, - - _updateRows(rows = [], options = {}) { - let state = { - isRefreshing: false, - paginationStatus: (options.allLoaded === true ? 'allLoaded' : 'waiting'), - }; - - if(options.mustSetLastManualRefreshAt) this.lastManualRefreshAt = new Date; - - if (rows !== null) { - this._setRows(rows); - - if (this.props.withSections === true) { - state.dataSource = this.state.dataSource.cloneWithRowsAndSections(rows); - } else { - state.dataSource = this.state.dataSource.cloneWithRows(rows); - } - } - - this.setState(state); - - //this must be fired separately or iOS will call onEndReached 2-3 additional times as - //the ListView is filled. So instead we rely on React's rendering to cue this task - //until after the previous state is filled and the ListView rendered. After that, - //onEndReached callbacks will fire. See onEndReached() above. - if(!this.firstLoadCompleteAt) this.firstLoadCompleteAt = new Date; - }, - _renderPaginationView() { let paginationEnabled = this.props.pagination === true || this.props.autoPaginate === true; From 048a79ec16ec14e896b5a2bb21130c42270b5eae Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Wed, 4 May 2016 00:18:02 -0700 Subject: [PATCH 4/9] remove logging --- GiftedListView.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index f192f20..63fefc9 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -274,9 +274,6 @@ var GiftedListView = React.createClass({ componentWillReceiveProps(nextProps, nextState) { let rows = nextProps.rows; - console.log("MAIN GIFTED", rows !== this.props.rows, JSON.stringify(nextProps.fetchOptions) !== JSON.stringify(this.props.fetchOptions)) - - if(rows !== this.props.rows) { if(this.beforeOptions && this.beforeOptions.paginatedFetch) { this._postPaginate(rows, {...this.beforeOptions, allLoaded: nextProps.allLoaded}); From 43a51382f211e1c647e8fab3b18dc971be94b8d1 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Tue, 10 May 2016 23:33:24 -0700 Subject: [PATCH 5/9] refreshcontrol maintaints its own state --- GiftedListView.js | 59 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 63fefc9..56c879a 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -12,6 +12,9 @@ var { ScrollView, } = React; +import shallowCompare from 'react-addons-shallow-compare'; + + // small helper function which merged two objects into one function MergeRecursive(obj1, obj2) { @@ -239,14 +242,14 @@ var GiftedListView = React.createClass({ mustSetLastManualRefreshAt: true, //we pass it along, so when the rows are updated we know to store the date as well }, options)); - //if(options.scrollToTop) this.scrollTo({y: -80}); //if you manually refresh the list, you often want to go to the top again, such as when filtering + this.scrollTo({y: 0}); //refreshing may be triggered by new fetchOptions while scrolled down, which should trigger scrolling to the top }, _onRefresh(options = {}) { + options = {...this.props.fetchOptions, ...options}; + if (this.isMounted()) { - this.setState({ - isRefreshing: true, - }); + this.refs.refreshControl.setIsRefreshing(true); this._setPage(1); this.refreshedAt = new Date; let page = this._getPage(); @@ -271,8 +274,9 @@ var GiftedListView = React.createClass({ //Configure props for `onFetch`, `fetchOptions` and 'rows' to declaratively change the rows displayed. //`onFetch` should now be used to dispatch a redux action, which reduces the `rows` prop :) - componentWillReceiveProps(nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { let rows = nextProps.rows; + let shouldUpdate = true; if(rows !== this.props.rows) { if(this.beforeOptions && this.beforeOptions.paginatedFetch) { @@ -286,17 +290,32 @@ var GiftedListView = React.createClass({ this._postRefresh(rows, this.beforeOptions); }, delay); } + + shouldUpdate = false; } //allow for declaratively refreshing simply by changing fetchOptions, //which will then call `onFetch` and if done right will result in new `rows` props, i.e. the above code. - else if(JSON.stringify(nextProps.fetchOptions) !== JSON.stringify(this.props.fetchOptions)) { + else if(nextProps.fetchOptions !== this.props.fetchOptions) { this.refresh(nextProps.fetchOptions); + shouldUpdate = false; + return false; } this.beforeOptions = {}; + return shouldUpdate ? shallowCompare(this, nextProps, nextState) : false; }, + shouldComponentUpdateOld(nextProps, nextState) { + if(nextProps.fetchOptions !== this.props.fetchOptions && nextProps.rows === this.props.rows) { + this.refresh(nextProps.fetchOptions); + return false; + } + + return shallowCompare(this, nextProps, nextState); + }, + + _postRefresh(rows = [], options = {}) { if (this.isMounted()) { this._updateRows(rows, options); @@ -305,7 +324,6 @@ var GiftedListView = React.createClass({ _updateRows(rows = [], options = {}) { let state = { - isRefreshing: false, paginationStatus: (options.allLoaded === true || rows.length === 0 ? 'allLoaded' : 'waiting'), }; @@ -322,6 +340,7 @@ var GiftedListView = React.createClass({ } this.setState(state, this.props.onRefresh); + this.refs.refreshControl.setIsRefreshing(false); //this must be fired separately or iOS will call onEndReached 2-3 additional times as //the ListView is filled. So instead we rely on React's rendering to cue this task @@ -421,9 +440,9 @@ var GiftedListView = React.createClass({ return this.props.renderRefreshControl({ onRefresh: this._onRefresh }); } return ( - + ); + } +} From ce4e520d71f843e62f1c52b07cfd87c2e7d85556 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Fri, 13 May 2016 04:47:41 -0700 Subject: [PATCH 6/9] making paging work with declarative fetchOptions --- GiftedListView.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 56c879a..00b6dd4 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -213,8 +213,7 @@ var GiftedListView = React.createClass({ }, componentDidMount() { - window.gifted = this; - this._fetch(this._getPage(), {firstLoad: true}); + this._fetch(this._getPage(), {firstLoad: true, ...this.props.fetchOptions}); //imperative OOP state utilized since onEndReached is imperatively called. So why waste cycles on rendering, which //can cause loss of frames in animation. @@ -263,8 +262,6 @@ var GiftedListView = React.createClass({ _fetch(page, beforeOptions, postCallback) { postCallback = postCallback || this._postRefresh; - Object.assign({}, beforeOptions, this.props.fetchOptions); //any fetch options will be passed along to `props.onFetch` in order to use for async queries - this.beforeOptions = beforeOptions; //will be used by componentWillReceive props; parent components only need to provide rows this.props.onFetch(page, (rows, options) => { @@ -280,7 +277,9 @@ var GiftedListView = React.createClass({ if(rows !== this.props.rows) { if(this.beforeOptions && this.beforeOptions.paginatedFetch) { - this._postPaginate(rows, {...this.beforeOptions, allLoaded: nextProps.allLoaded}); + setTimeout(() => { + this._postPaginate(rows, {...this.beforeOptions, allLoaded: nextProps.allLoaded}); + }, 1000); } else { let timeSinceRefresh = new Date - this.refreshedAt; //make sure at least 1 second goes by before hiding refresh control, @@ -298,7 +297,6 @@ var GiftedListView = React.createClass({ //which will then call `onFetch` and if done right will result in new `rows` props, i.e. the above code. else if(nextProps.fetchOptions !== this.props.fetchOptions) { this.refresh(nextProps.fetchOptions); - shouldUpdate = false; return false; } @@ -306,15 +304,6 @@ var GiftedListView = React.createClass({ return shouldUpdate ? shallowCompare(this, nextProps, nextState) : false; }, - shouldComponentUpdateOld(nextProps, nextState) { - if(nextProps.fetchOptions !== this.props.fetchOptions && nextProps.rows === this.props.rows) { - this.refresh(nextProps.fetchOptions); - return false; - } - - return shallowCompare(this, nextProps, nextState); - }, - _postRefresh(rows = [], options = {}) { if (this.isMounted()) { @@ -324,7 +313,7 @@ var GiftedListView = React.createClass({ _updateRows(rows = [], options = {}) { let state = { - paginationStatus: (options.allLoaded === true || rows.length === 0 ? 'allLoaded' : 'waiting'), + paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 ? 'allLoaded' : 'waiting'), }; if(options.mustSetLastManualRefreshAt) this.lastManualRefreshAt = new Date; @@ -392,9 +381,11 @@ var GiftedListView = React.createClass({ this.lastReleaseAt = new Date; }, _onPaginate() { + if(this.state.paginationStatus === 'allLoaded') return; + if (this.state.paginationStatus === 'firstLoad' || this.state.paginationStatus === 'waiting') { this.setState({paginationStatus: 'fetching'}); - this._fetch(this._getPage() + 1, {paginatedFetch: true}, this._postPaginate); + this._fetch(this._getPage() + 1, {paginatedFetch: true, ...this.props.fetchOptions}, this._postPaginate); } }, From 8fc6d1afec47112d24394f25394c74e7075d1fd0 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Fri, 13 May 2016 15:25:42 -0700 Subject: [PATCH 7/9] compare prev rows length to current rows length to determine allLoaded --- GiftedListView.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 00b6dd4..07289c3 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -213,7 +213,7 @@ var GiftedListView = React.createClass({ }, componentDidMount() { - this._fetch(this._getPage(), {firstLoad: true, ...this.props.fetchOptions}); + //this._fetch(this._getPage(), {firstLoad: true, ...this.props.fetchOptions}); //imperative OOP state utilized since onEndReached is imperatively called. So why waste cycles on rendering, which //can cause loss of frames in animation. @@ -313,9 +313,11 @@ var GiftedListView = React.createClass({ _updateRows(rows = [], options = {}) { let state = { - paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 ? 'allLoaded' : 'waiting'), + paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 || this._prevRowsLength === rows.length ? 'allLoaded' : 'waiting'), }; + this._prevRowsLength = rows.length; + if(options.mustSetLastManualRefreshAt) this.lastManualRefreshAt = new Date; if (rows !== null) { From 757a0676721a0bbc02197d02c851769752e21835 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Fri, 13 May 2016 15:29:38 -0700 Subject: [PATCH 8/9] small adjustment to prev rows length === rows .length comparison --- GiftedListView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index 07289c3..a85a026 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -307,13 +307,13 @@ var GiftedListView = React.createClass({ _postRefresh(rows = [], options = {}) { if (this.isMounted()) { - this._updateRows(rows, options); + this._updateRows(rows, options, true); } }, - _updateRows(rows = [], options = {}) { + _updateRows(rows = [], options = {}, isRefresh=false) { let state = { - paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 || this._prevRowsLength === rows.length ? 'allLoaded' : 'waiting'), + paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 || (this._prevRowsLength === rows.length && !isRefresh) ? 'allLoaded' : 'waiting'), }; this._prevRowsLength = rows.length; From 4b116d71683e893c50e9e5ba78c68a912d229579 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Tue, 17 May 2016 16:47:09 -0700 Subject: [PATCH 9/9] all loaded + infinite scroll optimization --- GiftedListView.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/GiftedListView.js b/GiftedListView.js index a85a026..b59080e 100644 --- a/GiftedListView.js +++ b/GiftedListView.js @@ -188,6 +188,7 @@ var GiftedListView = React.createClass({ getInitialState() { this._setPage(1); this._setRows([]); + this.refreshedAt = new Date; var ds = null; if (this.props.withSections === true) { @@ -213,7 +214,11 @@ var GiftedListView = React.createClass({ }, componentDidMount() { - //this._fetch(this._getPage(), {firstLoad: true, ...this.props.fetchOptions}); + //if(this.props.rows) { + // this._postRefresh(this.props.rows, this.beforeOptions); + //} + + this._fetch(this._getPage(), {firstLoad: true, ...this.props.fetchOptions}); //imperative OOP state utilized since onEndReached is imperatively called. So why waste cycles on rendering, which //can cause loss of frames in animation. @@ -313,7 +318,7 @@ var GiftedListView = React.createClass({ _updateRows(rows = [], options = {}, isRefresh=false) { let state = { - paginationStatus: (options.allLoaded === true || rows.length % this.props.limit !== 0 || (this._prevRowsLength === rows.length && !isRefresh) ? 'allLoaded' : 'waiting'), + paginationStatus: (options.allLoaded === true || rows.length === 0 || rows.length % this.props.limit !== 0 || (this._prevRowsLength === rows.length && !isRefresh || (this.props.limit && rows.length < this.props.limit)) ? 'allLoaded' : 'waiting'), }; this._prevRowsLength = rows.length;