We're live-coding on Twitch! Join us!
Implementing an Infinite Scroll list in React Native

Implementing an Infinite Scroll list in React Native

Code

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:

Essential Reading: Learn React from Scratch! (2019 Edition)
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 FlatListas 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:

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 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 been automatically loaded as you scroll down (see demo at start of 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 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

Like this article? Follow @brayoh_k on Twitter