Implementing an Infinite Scroll list in React Native
Publikováno: 4.2.2019
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 solutio...
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 beers catalogue API.
Demo Video
Here's a small demo video of what the end result will look like:
Setting Up
We will be using create-react-native-app
to bootstrap our React Native app, run the folowing command to install it globally:
npm install -g create-react-native-app
Next we need to bootstrap the app in your preffered 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 here.
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 dependecies 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 dependecies 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
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
Axios Config
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;
Card Styling
Next we will create cards to display our beer data and add some designs to it.
theme.js
This file contains the app color pallete 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 it's 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;
Fetching Beers
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 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
);
}
FlatList Component
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 most handy features this include:
- Fully cross-platform.
- Optional horizontal mode.
- Configurable viewability callbacks.
- Header support.
- Footer support.
- Separator support.
- Pull to Refresh.
- Scroll loading.
- ScrollToIndex support
We will be using a few features from the above list for our app namely footer, pull to refresh and scroll loading.
Basic Usage:
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 start 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:
Scroll Loading
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 which 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 start 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 been automatically loaded as you scroll down (see demo at start of tutorial).
Footer
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 demo at start of tutorial)
Pull To Refresh
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.
Extra Props Explained:
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
Conclusion:
Infinite scrolling grants your users a smooth experience while using you app and is an easy way for your to deliver presentable and well ordered content for your users.
You can access the code here