This tutorial is out of date and no longer maintained.
While implementing pagination in mobile devices, one has to take a different approach since space is minimal, unlike the web, due to this factor, infinite scrolling has always been the go-to solution, giving your users a smooth and desirable experience.
In this tutorial, we will be building an infinite scroll list using the FlatList component in React Native, we will be consuming Punk API which is a free beer catalog API.
Here’s a small demo video of what the end result will look like:
We will be using create-react-native-app
to bootstrap our React Native app, run the following command to install it globally:
- npm install -g create-react-native-app
Next, we need to bootstrap the app in your preferred directory:
- react-native init react_native_infinite_scroll_tutorial
I’ll be using an android emulator for this tutorial but the code works for both iOS and Android platforms. In case you don’t have an android emulator setup follow the instructions provided in the Android documentation.
Make sure your emulator is up and running then navigate to your project directory and run the following command:
- react-native run-android
This should download all required dependencies and install the app on your emulator and then launch it automatically, You should have a screen with the default text showing as follows:
Now that we have our sample app up and running, we will now install the required dependencies for the project, we will be using the Axios for making requests to the server and Glamorous Native for styling our components, run the following command to install them:
- npm install -S axios glamorous-native
Directory structure is always crucial in an application, since this is a simple demo app, we’ll keep this as minimal as possible:
src
├── App.js
├── components
│ ├── BeerPreviewCard
│ │ ├── BeerPreviewCard.js
│ │ └── index.js
│ ├── ContainedImage
│ │ └── index.js
│ └── Title
│ └── index.js
├── config
│ └── theme.js
└── utils
└── lib
└── axiosService.js
In order to make our Axios usage easy, we will create a singleton instance of the Axios service that we can import across our components:
import axios from 'axios';
const axiosService = axios.create({
baseURL: 'https://api.punkapi.com/v2/',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// singleton instance
export default axiosService;
Next, we will create cards to display our beer data and add some designs to it.
theme.js
This file contains the app color palette which we will use across the app.
export const colors = {
french_blue: '#3f51b5',
deep_sky_blue: '#007aff',
white: '#ffffff',
black: '#000000',
veryLightPink: '#f2f2f2'
};
Title.js
This file contains the card text component that we will use to display the beer name in the card.
import glamorous from 'glamorous-native';
import { colors } from '../../config/theme';
const Title = glamorous.text((props, theme) => ({
fontFamily: 'robotoRegular',
fontSize: 16,
color: props.color || colors.black,
lineHeight: 24,
textAlign: props.align || 'left',
alignSelf: props.alignSelf || 'center'
}));
export default Title;
ContainedImage.js
This file contains our image component which will have a resizeMode
of contained in order to have the image fit within its containing component.
import React from 'react';
import glamorous from 'glamorous-native';
const CardImageContainer = glamorous.view((props, theme) => ({
flex: 1,
alignItems: 'stretch'
}));
const StyledImage = glamorous.image((props, theme) => ({
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0
}));
const ContainedImage = props => {
return (
<CardImageContainer>
<StyledImage resizeMode="contain" {...props} />
</CardImageContainer>
);
};
export default ContainedImage;
BeerPreviewCard.js
This file contains the main card container, this is where we combine the title component and the image component to form a card that displays the beer name and image.
import React from 'react';
import glamorous from 'glamorous-native';
// app theme colors
import { colors } from '../../config/theme';
// components
import Title from '../Title';
import ContainedImage from '../ContainedImage';
const CardContainer = glamorous.view((props, theme) => ({
height: 160,
width: '85%',
left: '7.5%',
justifyContent: 'space-around'
}));
const CardImageContainer = glamorous.view((props, theme) => ({
flex: 1,
alignItems: 'stretch'
}));
const BeerNameContainer = glamorous.view((props, theme) => ({
height: '30%',
backgroundColor: colors.deep_sky_blue,
justifyContent: 'center'
}));
const BeerPreviewCard = ({ name, imageUrl }) => {
return (
<CardContainer>
<CardImageContainer>
<ContainedImage source={{ uri: imageUrl }} />
</CardImageContainer>
<BeerNameContainer>
<Title align="center" color={colors.white}>
{name}
</Title>
</BeerNameContainer>
</CardContainer>
);
};
export default BeerPreviewCard;
The logic for fetching beers will be in App.js
which is the main component of the app, we need to consume the API by making a GET request to fetch a list of paginated beers:
import React, { Component } from 'react';
// axios service
import axiosService from './utils/lib/axiosService';
export default class AllBeersScreen extends Component {
state = {
data: [],
page: 1,
loading: true,
error: null
};
componentDidMount() {
this._fetchAllBeers();
}
_fetchAllBeers = () => {
const { page } = this.state;
const URL = `/beers?page=${page}&per_page=10`;
axiosService
.request({
url: URL,
method: 'GET'
})
.then(response => {
this.setState((prevState, nextProps) => ({
data:
page === 1
? Array.from(response.data)
: [...this.state.data, ...response.data],
loading: false
}));
})
.catch(error => {
this.setState({ error, loading: false });
});
};
render() {
return (
// map through beers and display card
);
}
}
So what is a FlatList component? I’ll quote the React Native docs which describes it as a performant interface for rendering simple, flat lists, supporting the following features:
We will be using a few features from the above list for our app namely footer, pull to refresh, and scroll loading.
To use the FlatList component, you have to pass two main props which are RenderItem
and data
We can now pass the data we fetched earlier on to the FlatList
component and use the BeerPreviewCard
component to render a basic FlatList
as follows:
export default class AllBeersScreen extends Component {
// fetch beer request and update state from earlier on
render() {
return (
<FlatList
contentContainerStyle={{
flex: 1,
flexDirection: 'column',
height: '100%',
width: '100%'
}}
data={this.state.data}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<View
style={{
marginTop: 25,
width: '50%'
}}
>
<BeerPreviewCard name={item.name} imageUrl={item.image_url} />
</View>
)}
/>
);
}
Reload your app and you should a view similar to this:
The main feature of infinite scrolling is loading content on-demand as the user scrolls through the app, to achieve this, the FlatList
component requires two props namely onEndReached
and onEndReachedThreshold
.
onEndReached
is the callback called when the users scroll position is close to the onEndReachedThreshold
of the rendered content, onEndReachedThreshold
is basically a number that indicates the user’s scroll position in relation to how far it is from the end of the visible content when the user reaches the specified position, the onEndReached
callback is triggered.
A value of 0.5
will trigger onEndReached
when the end of the content is within half the visible length of the list, which is what we need for this use case.
export default class AllBeersScreen extends Component {
state = {
data: [],
page: 1,
loading: true,
loadingMore: false,
error: null
};
// fetch beer request and update state from earlier on
_handleLoadMore = () => {
this.setState(
(prevState, nextProps) => ({
page: prevState.page + 1,
loadingMore: true
}),
() => {
this._fetchAllBeers();
}
);
};
render() {
return (
<FlatList
contentContainerStyle={{
flex: 1,
flexDirection: 'column',
height: '100%',
width: '100%'
}}
data={this.state.data}
renderItem={({ item }) => (
<View
style={{
marginTop: 25,
width: '50%'
}}
>
<BeerPreviewCard name={item.name} imageUrl={item.image_url} />
</View>
)}
onEndReached={this._handleLoadMore}
onEndReachedThreshold={0.5}
initialNumToRender={10}
/>
);
}
}
If you go back to the app and scroll down, you’ll notice the beer list has been automatically loaded as you scroll down (see the demo at the start of the tutorial).
The footer is basically the bottom part of our FlatList
component, when the user scrolls down we want to show a loader when the content is been fetched, we can achieve this using the ListFooterComponent
prop where we will pass a function that returns an ActivityIndicator component wrapped in a View component:
_renderFooter = () => {
if (!this.state.loadingMore) return null;
return (
<View
style={{
position: 'relative',
width: width,
height: height,
paddingVertical: 20,
borderTopWidth: 1,
marginTop: 10,
marginBottom: 10,
borderColor: colors.veryLightPink
}}
>
<ActivityIndicator animating size="large" />
</View>
);
};
render() {
return (
<FlatList
// other props
ListFooterComponent={this._renderFooter}
/>
);
}
Now when scrolling a loader will show on the screen while the content is loading (see the demo at the start of the tutorial)
Pull to refresh functionality is widely used in almost every modern application that uses network activity to fetch data, to achieve this in the FlatList
, we need to pass the onRefresh
prop which triggers a callback when the user carries a pull-down gesture at the top of the screen:
_handleRefresh = () => {
this.setState(
{
page: 1,
refreshing: true
},
() => {
this._fetchAllBeers();
}
);
};
render() {
return (
<FlatList
// other props
onRefresh={this._handleRefresh}
refreshing={this.state.refreshing}
/>
);
}
Now when you try pulling down from the top part of the screen a loader will appear from the top and the content will be refetched.
initialNumToRender
- This is the number of items we want to render when the app loads the data.
keyExtractor
- Used to extract a unique key for a given item at the specified index.
Infinite scrolling grants your users a smooth experience while using your app and is an easy way for you to deliver presentable and well-ordered content for your users.
You can access the code here.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!