Building Pull to Refresh Android iOS Using React Native

Pull to refresh is a widely adopted feature in mobile apps. Facebook, Gmail, Instagram almost all popular apps have this feature where the user can drag the screen downward that signals the server to send new data.

We have already covered the infinite scroll implementation for Android and iOS. Check out the article here.

Prerequisites and Target Audience

Developers having a basic understanding of frontend and backend coding and development environment.
Basic node and npm commands (for backend simulation), expo setup (for frontend implementation).

Why you should have this feature

Nowadays considering how easily the user can operate the application determines how many users might be using your application.

Also, cleaner the User Interface more likable is the application to the user. Now suppose your page feed is loaded but now user wants to reload the feed to check on if there are any new elements in your feed.

So, as a user, it will be quite convenient to pull down at the top of the page and reload instead of reopening the application.

The application flow

We are going to develop our mobile app which will run on Android and iOS. We are going to use react native framework.

For the backend server, we will use simple Nodejs mock server. You can replace the same with real data with few tweaks and reuse the code.

Upon each pull to refresh request, Nodejs server will provide the fresh mock dataset to showcase on the app.

Let’s code.

Setting up Backend

Create a new directory and switch to it using the terminal.

Create a new Node project using the following command.

$ npm init --y

Install the required dependencies using the following command.

$ npm i --S express

Create a new file and name it app.js and copy/paste the following code.

app.js
const express = require('express')
const app = express()
const router = express.Router()

router.get('/fetch-paginated-data', (req, res) => {
    var pageNo = parseInt(req.query.pageNo)
    var pageSize = parseInt(req.query.pageSize)
    //checking if page number is invalid
    if (pageNo <= 0) {
        var response = {
            success: false,
            message: 'Invalid Page Number'
        };
        return res.status(200).json(response);
    } else {
        //fetch data from database based on given page no and page size
        var index = (parseInt(pageNo - 1) * parseInt(pageSize)) + 1;
        var list = [];
        for (var i = 0; i < pageSize - 1; i++) {
            list.push({
                index: index,
                data: 'Data ' + index
            });
            index++;
        }
        var response = {
            success: true,
            list: list
        };
        return res.status(200).json(response);
    }

});

app.use('/', router)

app.listen(3000);

Run the backend app:

node app.js

Now we have a backend simulation ready which will keep on giving us data for any page number and any page size request.

Building the Mobile App

We are going to use react native framework to code our mobile app. We can use multiple tools to use this framework. In this tutorial we are going to use expo.

You can install expo cli if you have not already by running the following command.

$ npm i expo-cli --global

Then, create a new expo project using the following command.

$ expo init

It will ask you two questions. Fill up the proper name and slug URL for your app.

? Choose a project name: pull_to_refresh_infinite_scroll_demo
? Choose a template: expo-template-blank

Now switch to the folder and install the dependencies.

npm i --save axios native-base @expo/vector-icons --save

Open the App.js and copy/paste the following code.

App.js
import React from 'react';
import { StyleSheet, ActivityIndicator, View,RefreshControl } from 'react-native';
import { Container, Content, Text, List } from 'native-base';
import axios from 'axios';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    //initialize state values
    this.state = {
      pageNo: 1,
      pageSize: 20,
      showLoadingMore:false,
      data: [],
      loadMoreData: true,//to denote whether bottom of list is reached
      shouldHit: true, //whether more data needs to be fetched
      dataReceived: false, //whether initial data is fetched
      refreshing:false
    }
  }
  componentWillMount = () => {
    this.fetchData();
  }

  //function to fetch more data as per current page number
  fetchData = () => {
    if(this.state.pageNo!=1){
      //when we try to fetch more data show loader at the bottom
      this.setState({
        showLoadingMore:true
      })
    }
    var systemIPAddress='192.168.1.25'; // add your IP.
    var url = 'http://'+systemIPAddress+':3000/fetch-paginated-data?pageNo='+this.state.pageNo+'&pageSize='+this.state.pageSize;
    axios
      .get(url)
      .then(response => {
        if (response.data.success) {
          //add data to list and change the state to render new content
          let receivedDataList = response.data.list;
          let currentDataList = this.state.data;
          //append to existing list
          let newDataList = currentDataList.concat(receivedDataList);
          //render new list
          //once new list is set we are ready to load more data if bottom is reached
          let loadMoreData =true;
          if(this.state.refreshing){
            this.setState({
              pageNo:this.state.pageNo+1,
              data:newDataList,
              dataReceived:true,
              loadMoreData: loadMoreData,
              showLoadingMore:false,
              refreshing:false
            })
          }else{
            this.setState({
              pageNo:this.state.pageNo+1,
              data:newDataList,
              dataReceived:true,
              loadMoreData: loadMoreData,
              showLoadingMore:false,
            })
          }
         
        } else {
          //no more data to be loaded
          this.setState({
            shouldHit:false,
            showLoadingMore:false,
            refreshing:false
          })
        }
      })
      .catch(error => {
        console.log(error)
      });
  }

  onRefreshHandler = () => {
    //reset pageNo to 1
    this.setState({refreshing: true,pageNo:1,data:[],dataReceived:false});
    let reactNativeInstance = this;
    //timeout to simulate loading
    setTimeout(()=>{
      reactNativeInstance.fetchData();
    },100);
   
  }

  render() {
   
    const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }) => {
      const paddingToBottom = 40;
      let result = layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom;
      //true if the end is reached other wise false
      return result;
    };

    //initially display loader at the center
    let listSection =
    <View style={ styles.container} >
      <ActivityIndicator size="large" color="#0000ff" />
    </View>

    if (this.state.dataReceived) {
      if (this.state.data.length > 0) {
        listSection = this
          .state
          .data
          .map((record) => {
            return (
              <View key={record.index} style={[styles.container,{ margin: 10, height:40, borderWidth:1, borderColor:'black'}]}>
                <Text>{record.index}. {record.data}</Text>
              </View>
            );
          })
      } else {
        listSection = null;
      }
    }

    if (this.state.refreshing) {
      return (
        <View style={styles.container}>
          <Text>Refreshing Please Wait ...</Text>
        </View>
      )
    } else if (this.state.dataReceived && this.state.data.length == 0) {
      return (
        <View style={styles.container}>
          <Text>No records to display</Text>
        </View>
      )
    } else {
      return (
        <Container style={{marginTop:40}}>
          <Content
          refreshControl={
            <RefreshControl
            refreshing={this.state.refreshing}
            onRefresh={this.onRefreshHandler}
          />
          }
          onScroll={({ nativeEvent }) => {
            if (isCloseToBottom(nativeEvent)) {
              //prevent multiple hits for same page number
              if(this.state.loadMoreData){
                //bottom reached start loading data
                this.setState({
                  loadMoreData:false
                })
                this.fetchData();
              }
             
            }
          }}>
            <List>
              {listSection}
            </List>
            {this.state.showLoadingMore ? <ActivityIndicator size="large" color="#0000ff" />: null}
           
          </Content>

        </Container>
      )
    }

  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Pull to refresh Explanation

Import and Setup RefreshControl component as a JSX attribute for Content native-base component.

<Content
         refreshControl={
           <RefreshControl
           refreshing={this.state.refreshing}
           onRefresh={this.onRefreshHandler}
         />

RefreshControl component has two attributes:

  1. refreshing: when true implies the user has attempted to refresh the list.
  2. onRefresh: points to the handler which handles the execution when the user clicks refresh.

Handler for onRefresh Event resets various state values to initial values and the code is as follows:

onRefreshHandler = () => {
   //reset pageNo to 1
   this.setState({refreshing: true,pageNo:1,data:[],dataReceived:false});
   let reactNativeInstance = this;
   //timeout to simulate loading
   setTimeout(()=>{
     reactNativeInstance.fetchData();
   },300);  
 }

Let’s run the app.

$ npm start

A new browser window will open, something like this.

Download the expo mobile app available on Google Play and iTunes store. Here is the link.

https://play.google.com/store/apps/details?id=host.exp.exponent&hl=en (For Android Phones)
https://itunes.apple.com/us/app/expo-client/id982107779?mt=8 (For iPhone)

Scan the QR code using the expo app and our custom app will run on your mobile phone.

Here is the demo video link.

Conclusion

If your mobile app is dealing with data then pull to refresh feature is a must. It’s interactive, useful and people sort of know how to use it already.

Shahid
Shahid

Founder of Codeforgeek. Technologist. Published Author. Engineer. Content Creator. Teaching Everything I learn!

Articles: 126