Skip to content

Instantly share code, notes, and snippets.

@froger
Last active October 23, 2016 17:55
Show Gist options
  • Save froger/7d75f5bc126e156d218f348414ba2bf0 to your computer and use it in GitHub Desktop.
Save froger/7d75f5bc126e156d218f348414ba2bf0 to your computer and use it in GitHub Desktop.
Make react-infinite-scroll async.
/*!
* React v15.0.0
* Compile with babel-presets: ['babel-preset-es2015', 'react']. @see https://babeljs.io/docs/plugins/#presets
*
* Refactoring from react-infinite-scroll - v 0.1.3 - guillaumervls 2014-04-07
* @see https://www.npmjs.com/package/react-infinite-scroll
*
* Changes from react-infinite-scroll
* ====
*
* - Put a debounce timeout, too not send too much loadMore (as the scroll event is very fast)
* - Put a finished callback, to easely put some informations at the bottom of the infinite list
* when she is finished (if she is once)
* - Allow async loadMore, with Promise. (look https://github.com/calvinmetcalf/lie for better cross-browser compatibilty)
* - Refactor for React v15.0.0, in ES2015
*
* Required Props
* ===
*
* @param {number} pageStart Where should the pagination index start
* @param {bool} hasMore If the Scroller need to continue asking for new resources
* @param {func} loadMore A function that return a Promise. Will stop loading on error.
* @param {}
*
* Optional Props
* ===
*
* @param {func} finished Callback when there are no more items to load
* @param {func} onError Callback when the loadMore promise is rejected.
* @param {number} threshold Set it when you want to put your list after the top of the page.
* Will try to guess a OK value if not provided.
* @param {any} children The loaded components to show in the list
* @param {string} className Class names to add to the infinite scroll list.
* Will add :
* - `is-finished` when there is no more to load (or there were an error)
* - `has-more` when there are still things to load (from this.props.hasMore)
* - `js-page-#{pagination}` the number of this.loadMore call we have done yet
* - `js-infinite_scroll` Just a class to be referenced by js.
*
* I personally use `js-` prefix to manipulate classes in JS
* to stay independant from my styles.
*/
var React = require("react");
/**
* Bind the `this` of a given instance to the given methods
*
* Example :
* `bind(my_instance, "toString", "callMe");`
*
* @param {Object} Instance to bind
* @param {...String} Methods to bind
* @return {void}
*/
const bind = function(obj) {
var methods = [].slice.call(arguments, 1);
for(var i = methods.length - 1; i >= 0; i--){
var method = methods[i];
if(typeof obj[method] === "undefined")
throw new Error(method + " not defined");
else if(typeof obj[method] !== "function")
throw new Error(method + " is not a function");
obj[method] = obj[method].bind(obj);
}
};
/**
* Calculate the position from the top of the page.
* Will recursively add the offset top of the parent element.
*
* @param {DOMNode} dom_element the node we want to know the top position
* @return {number} the top position in px.
*/
const topPosition = function(dom_element) {
if (!dom_element) {
return 0;
}
return dom_element.offsetTop + topPosition(dom_element.offsetParent);
}
export default class extends React.Component {
static displayName = "InfiniteScroll";
static defaultProps = {
className: "",
finished: function(){},
onError: function(){},
loader: (<span className="loader">Loading…</span>)
};
/**
* @see the file comment for props docs.
* @type {obj}
*/
static propTypes = {
pageStart: React.PropTypes.number.isRequired,
hasMore: React.PropTypes.bool.isRequired,
loadMore: React.PropTypes.func.isRequired,
finished: React.PropTypes.func,
onError: React.PropTypes.func,
threshold: React.PropTypes.number,
children: React.PropTypes.any,
loader: React.PropTypes.object,
className: React.PropTypes.string
};
constructor(){
super();
this.state = {
isLoading: false,
threshold: this.props.threshold,
pagination: this.props.pageStart,
finished: false
};
bind(this, "attachScrollListener",
"detachScrollListener",
"scrollListener")
this.reference = null;
this.debounceTimeout = null;
this.debounceDelay = 250;
}
componentDidUpdate(prevProps){
// If we had no more item before and now we have.
if(!prevProps.hasMore && this.props.hasMore){
this.setState({ finished: false });
this.attachScrollListener();
}
}
componentDidMount(){
if(typeof this.props.threshold !== "undefined"){
this.setState({ threshold: nextProps.threshold });
}else{
// Guess some good threshold value for our list.
this.setState({ threshold: topPosition(this.reference) });
}
// Start listening to scroll
this.attachScrollListener();
}
/**
* IF the page we want has increased, we want to go back listen to scroll.
* ELSE we still are fetching async data with loadmore.
* @param {object} nextProps The new props the component receive
* @return {void}
*/
componentWillReceiveProps(nextProps){
if(this.props.hasMore && !nextProps.hasMore){
this.setState({ finished: true });
nextProps.finished();
return;
}
if(typeof nextProps.threshold !== "undefined" &&
nextProps.threshold !== this.state.threshold)
this.setState({ threshold: nextProps.threshold });
if(nextProps.hasMore)
this.attachScrollListener();
}
attachScrollListener() {
if(!this.props.hasMore)
return;
window.addEventListener("scroll", this.scrollListener);
window.addEventListener("resize", this.scrollListener);
this.scrollListener();
}
detachScrollListener() {
window.removeEventListener("scroll", this.scrollListener);
window.removeEventListener("resize", this.scrollListener);
}
scrollListener() {
if(this.debounceTimeout !== null &&
typeof debounceTimeout !== "undefined" &&
!this.state.finished)
return; // We can not accomplish the scroll callback action for now.
this.debounceTimeout = setTimeout(() => {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = null;
var el = this.reference;
var scrollTop =
(typeof window.pageYOffset !== "undefined") ? window.pageYOffset
: (document.documentElement ||
document.body.parentNode ||
document.body).scrollTop;
if(!this.state.isLoading && this.props.hasMore){
if(topPosition(el)
+ el.offsetHeight
- scrollTop
- window.innerHeight < Number(this.state.threshold)) {
this.detachScrollListener();
this.setState({ isLoading: true });
// Loading always return a promise, to be able to do
// sync process or async process
this.props.loadMore(this.state.pagination + 1).then(() => {
this.setState({
isLoading: false,
pagination: this.state.pagination + 1
})
this.attachScrollListener();
}).catch((error) => {
this.props.onError(error);
this.setState({
isLoading: false,
pagination: this.state.pagination + 1,
finished: true
})
});
}
}
}, this.debounceDelay);
}
componentWillUnmount() {
// The component will disapear, stop listening to scroll
this.detachScrollListener();
}
render(){
var additionalClassName = _.isUndefined(this.props.className) ? "" : " " + this.props.className;
return <section
className={"js-infinite_scroll" + additionalClassName +
" js-page-" + this.props.pageStart +
(this.props.hasMore ? " has-more" : " is-finished")
}
ref={(reference) => {
if(reference !== null &&
typeof reference !== "undefined" &&
this.reference === null)
this.reference = reference;
}}>
{ this.props.children }
{ this.state.isLoading && this.props.loader }
</section>
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment