InfiniteScrollView is a React Native scroll view that notifies you as the scroll offset approaches the bottom. You can instruct it to display a loading indicator while you load more content. This is a common design in feeds. InfiniteScrollView also supports horizontal scroll views.
It conforms to ScrollableMixin so you can compose it with other scrollable components.
npm install --save react-native-infinite-scroll-view
Compose InfiniteScrollView with the scrollable component that you would like to get events from. In the case of a basic ListView, you would write:
import React from 'react';
import {
ListView,
} from 'react-native';
import InfiniteScrollView from 'react-native-infinite-scroll-view';
class ExampleComponent extends React.Component {
_loadMoreContentAsync = async () => {
// Fetch more data here.
// After fetching data, you should update your ListView data source
// manually.
// This function does not have a return value.
}
render() {
return (
<ListView
renderScrollComponent={props => <InfiniteScrollView {...props} />}
dataSource={...}
renderRow={...}
canLoadMore={this.state.canLoadMoreContent}
onLoadMoreAsync={this._loadMoreContentAsync}
/>
);
}
}
A more complete example that uses a ListView.DataSource
, react-redux, and supports pagination would look something like this:
import React from 'react';
import {
ListView,
RefreshControl,
} from 'react-native';
import InfiniteScrollView from 'react-native-infinite-scroll-view';
import { connect } from 'react-redux';
class ExampleComponent extends React.Component {
static propTypes = {
// Assume data shape looks like:
// {items: ["item1", "item2"], nextUrl: null, isFetching: false}
listData: PropTypes.object.isRequired,
// dispatch is automatically provided by react-redux, and is used to
// interact with the store.
dispatch: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: this._rowHasChanged.bind(this),
}),
};
// Update the data store with initial data.
this.state.dataSource = this.getUpdatedDataStore(props);
}
async componentWillMount() {
// Initial fetch for data, assuming that listData is not yet populated.
this._loadMoreContentAsync();
}
componentWillReceiveProps(nextProps) {
// Trigger a re-render when receiving new props (when redux has more data).
this.setState({
dataSource: this.getUpdatedDataSource(nextProps),
});
}
getUpdatedDataSource(props) {
// See the ListView.DataSource documentation for more information on
// how to properly structure your data depending on your use case.
let rows = props.listData.items;
let ids = rows.map((obj, index) => index);
return this.state.dataSource.cloneWithRows(rows, ids);
}
_rowHasChanged(r1, r2) {
// You might want to use a different comparison mechanism for performance.
return JSON.stringify(r1) !== JSON.stringify(r2);
}
_renderRefreshControl() {
// Reload all data
return (
<RefreshControl
refreshing={this.props.listData.isFetching}
onRefresh={this._loadMoreContentAsync.bind(this)}
/>
);
}
_loadMoreContentAsync = async () => {
// In this example, we're assuming cursor-based pagination, where any
// additional data can be accessed at this.props.listData.nextUrl.
//
// If nextUrl is set, that means there is more data. If nextUrl is unset,
// then there is no existing data, and you should fetch from scratch.
this.props.dispatch(fetchMoreContent(this.props.listData.nextUrl));
}
render() {
return (
<ListView
renderScrollComponent={props => <InfiniteScrollView {...props} />}
dataSource={this.state.dataSource}
renderRow={...}
refreshControl={this._renderRefreshControl()}
canLoadMore={!!this.props.listData.nextUrl}
onLoadMoreAsync={this._loadMoreContentAsync.bind(this)}
/>
);
}
}
const mapStateToProps = (state) => {
return {listData: state.listData};
};
export default connect(mapStateToProps)(ExampleComponent);
- Horizontal scroll views are supported
- When you load more content in an infinite ListView, the ListView by default will render only one row per frame. This means that for a short amount of time after loading new content, the user could still be very close to the bottom of the scroll view and may trigger a second load.
- Known issue: Make sure your initial data reaches the bottom of the screen, otherwise scroll events won't trigger. Subsequent loads are not affected. See expo/react-native-infinite-scroll-view#9 for more details.
InfiniteScrollView uses the onScroll
event to continuously calculate how far the scroll offset is from the bottom.