Tutorial

Implementing an Infinite Scroll list in React Native

Draft updated on Invalid Date
Default avatar

By Brian Njenga

Implementing an Infinite Scroll list in React Native

This tutorial is out of date and no longer maintained.

Introduction

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.

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 following command to install it globally:

  1. npm install -g create-react-native-app

Next, we need to bootstrap the app in your preferred directory:

  1. 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:

  1. 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:

  1. 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 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;

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 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
    );
  }
}

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 following features:

  • 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 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 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

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 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.

Learn more about us


About the authors
Default avatar
Brian Njenga

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel