react-native-infinite-scroll-view
Version:
An infinitely scrolling view that notifies you as the scroll offset approaches the bottom
146 lines (121 loc) • 4.14 kB
JavaScript
'use strict';
import PropTypes from 'prop-types';
import React from 'react';
import { ScrollView, View } from 'react-native';
import ScrollableMixin from 'react-native-scrollable-mixin';
import cloneReferencedElement from 'react-clone-referenced-element';
import DefaultLoadingIndicator from './DefaultLoadingIndicator';
export default class InfiniteScrollView extends React.Component {
static propTypes = {
...ScrollView.propTypes,
distanceToLoadMore: PropTypes.number.isRequired,
canLoadMore: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).isRequired,
onLoadMoreAsync: PropTypes.func.isRequired,
onLoadError: PropTypes.func,
renderLoadingIndicator: PropTypes.func.isRequired,
renderLoadingErrorIndicator: PropTypes.func.isRequired,
};
static defaultProps = {
distanceToLoadMore: 1500,
canLoadMore: false,
scrollEventThrottle: 100,
renderLoadingIndicator: () => <DefaultLoadingIndicator />,
renderLoadingErrorIndicator: () => <View />,
renderScrollComponent: props => <ScrollView {...props} />,
};
constructor(props, context) {
super(props, context);
this.state = {
isDisplayingError: false,
};
this._handleScroll = this._handleScroll.bind(this);
this._loadMoreAsync = this._loadMoreAsync.bind(this);
}
getScrollResponder() {
return this._scrollComponent.getScrollResponder();
}
setNativeProps(nativeProps) {
this._scrollComponent.setNativeProps(nativeProps);
}
render() {
let statusIndicator;
if (this.state.isDisplayingError) {
statusIndicator = React.cloneElement(
this.props.renderLoadingErrorIndicator({ onRetryLoadMore: this._loadMoreAsync }),
{ key: 'loading-error-indicator' }
);
} else if (this.state.isLoading) {
statusIndicator = React.cloneElement(this.props.renderLoadingIndicator(), {
key: 'loading-indicator',
});
}
let { renderScrollComponent, ...props } = this.props;
Object.assign(props, {
onScroll: this._handleScroll,
children: [this.props.children, statusIndicator],
});
return cloneReferencedElement(renderScrollComponent(props), {
ref: component => {
this._scrollComponent = component;
},
});
}
_handleScroll(event) {
if (this.props.onScroll) {
this.props.onScroll(event);
}
if (this._shouldLoadMore(event)) {
this._loadMoreAsync().catch(error => {
console.error('Unexpected error while loading more content:', error);
});
}
}
_shouldLoadMore(event) {
let canLoadMore =
typeof this.props.canLoadMore === 'function'
? this.props.canLoadMore()
: this.props.canLoadMore;
return (
!this.state.isLoading &&
canLoadMore &&
!this.state.isDisplayingError &&
this._distanceFromEnd(event) < this.props.distanceToLoadMore
);
}
async _loadMoreAsync() {
if (this.state.isLoading && __DEV__) {
throw new Error('_loadMoreAsync called while isLoading is true');
}
try {
this.setState({ isDisplayingError: false, isLoading: true });
await this.props.onLoadMoreAsync();
} catch (e) {
if (this.props.onLoadError) {
this.props.onLoadError(e);
}
this.setState({ isDisplayingError: true });
} finally {
this.setState({ isLoading: false });
}
}
_distanceFromEnd(event) {
let { contentSize, contentInset, contentOffset, layoutMeasurement } = event.nativeEvent;
let contentLength;
let trailingInset;
let scrollOffset;
let viewportLength;
if (this.props.horizontal) {
contentLength = contentSize.width;
trailingInset = contentInset.right;
scrollOffset = contentOffset.x;
viewportLength = layoutMeasurement.width;
} else {
contentLength = contentSize.height;
trailingInset = contentInset.bottom;
scrollOffset = contentOffset.y;
viewportLength = layoutMeasurement.height;
}
return contentLength + trailingInset - scrollOffset - viewportLength;
}
}
Object.assign(InfiniteScrollView.prototype, ScrollableMixin);