Onuorah Bonaventure Full-stack web developer that loves connecting brands to their customers by developing amazing, performant websites. Interested in teaching programming concepts and breaking down difficult concepts. When I'm not coding, I play chess and checkers with my friends while listening to cool Afrobeats.

Add refresh functionality to your React Native apps

14 min read 4057 107

Adding Refresh Functionality React Native Apps

Given the rise in user expectations for flawless digital experiences, it’s important to ensure your mobile applications function intuitively and correctly. For example, when mobile users reach the bottom of a scrolling list and continue pulling down, they anticipate that the app will automatically fetch the next set of results.

In this article, we’ll investigate how to implement pull-to-refresh and scroll-to-refresh functionality in a React Native application using a FlatList component. As a bonus, we’ll examine how to customize a RefreshControl component by changing different parameters, like size and color.

In the tutorial portion of this article, we’ll build a React Native application that refreshes like this:

React Native App Pull-To-Refresh Scroll-To-Refresh Functionality
React Native application with pull-to-refresh and scroll-to-refresh functionality.

Jump ahead:

Project setup

To follow along with this tutorial, you can refer to this GitHub repo; it contains all of the code used in this article. However, I also recommend you generate an entirely new Expo React Native application by following this guide.

Once the project is generated, we’ll need to create some folders. Here’s how the folder structure will look:

// Folder structure

app_name
 ┣ assets
 ┃ ┣ adaptive-icon.png
 ┃ ┣ favicon.png
 ┃ ┣ icon.png
 ┃ ┗ splash.png
 ┣ components
 ┃ ┣ Item.jsx
 ┃ ┗ Loader.jsx
 ┣ hooks
 ┃ ┗ useFetchUser.js
 ┣ screens
 ┃ ┣ BottomLoader.jsx
 ┃ ┣ Combined.jsx
 ┃ ┗ TopLoader.jsx
 ┣ .gitignore
 ┣ App.js
 ┣ app.json
 ┣ babel.config.js
 ┣ package-lock.json
 ┗ package.json

Creating the custom Hook

A React Hook is a special kind of function that allows us to manage states without writing a class or a component. We’ll set up a custom Hook to make API calls to a server to fetch data and then store it in a state to be used in our components.

To start, install Axios by running the following command in the terminal:

npm i axios

Then open the hooks/useFetchUser.js file, import necessary dependencies, and declare some constants:

// JavaScript
// useFetchUser.js

import React, { useState } from 'react';
import axios from 'axios';

export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;

Next, let’s create the useFetchUser Hook and declare some states:

// JavaScript
// useFetchUser.js

export const useFetchUser = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [success, setSuccess] = useState(false);

// Code comes below here

// Code comes above here

  return { users, isLoading, success, getUsers, errorMessage };
};

Now, we’ll create a function that will enable us to make API calls and update the states based on the responses. For this example, we’ll make use of the pagination API from https://randomuser.me/api:

// JavaScript
// useFetchUser.js

 const getUsers = async (currentPage, isTop = false) => {
    setIsLoading(true);
    setSuccess(false);
    setErrorMessage('');
    try {
      const { data } = await axios.get(
        `https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
      );
      if (isTop) {
        const newList = [...data?.results, ...users];
        const slicedUsers = [...newList].slice(0, MAX_LENGTH);
        setUsers(slicedUsers);
      }
      if (!isTop) {
        const randomIndex = () => Math.ceil(Math.random() * 10);
        const newList = [...users, ...data?.results];
        const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
        setUsers(slicedUsers);
      }
      setSuccess(true);
    } catch (error) {
      const theError =
        error?.response && error.response?.data?.message
          ? error?.response?.data?.message
          : error?.message;
      setErrorMessage(theError);
    }
    setIsLoading(false);
  };

Combined, the code should look similar to this:

// JavaScript
// useFetchUser.js

import React, { useState } from 'react';
import axios from 'axios';
export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;

export const useFetchUser = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [success, setSuccess] = useState(false);

  const getUsers = async (currentPage, isTop = false) => {
    setIsLoading(true);
    setSuccess(false);
    setErrorMessage('');
    try {
      const { data } = await axios.get(
        `https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
      );
      if (isTop) {
        const newList = [...data?.results, ...users];
        const slicedUsers = [...newList].slice(0, MAX_LENGTH);
        setUsers(slicedUsers);
      }
      if (!isTop) {
        const randomIndex = () => Math.ceil(Math.random() * 10);
        const newList = [...users, ...data?.results];
        const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
        setUsers(slicedUsers);
      }
      setSuccess(true);
    } catch (error) {
      const theError =
        error?.response && error.response?.data?.message
          ? error?.response?.data?.message
          : error?.message;
      setErrorMessage(theError);
    }
    setIsLoading(false);
  };

  return { users, isLoading, success, getUsers, errorMessage };
};

Here are some details to better understand what’s going on in the above code. The getUsers function is asynchronous and accepts currentPage and isTop as parameters. The currentPage parameter is passed to the URL as instructed in https://randomuser.me in order to fetch the paginated items. The isTop parameter is used to determine if the user is refreshing content from the top or from the bottom.

We set the loading, success, and error message states to true, false, and an empty string, respectively. Next, we create a try-catch block. In the try part, we make an API call with axios and pass FETCH_RESULTS to the URL. We then expect the length of the data returned from the response to equal the value of FETCH_RESULTS. In other words, if the value of FETCH_RESULTS is 10, the API will return 10 results.

In the first if statement, we check that isTop is true. Inside the code block, we create one list that prepends to the existing users array and a second list that slices our newList to ensure we are within the desired MAX_LENGTH of data before actually updating the user list.

In the second if statement, we create an inline function to generate a random number. Next, we append the newly received data to the existing user list and then slice the list we’re creating to get a relatively random number of list results all the time. This ensures that users do not experience any flickering or stuttering when scrolling from the bottom of the list.

In the catch block, we extract any error and then set it to the error message state; outside the try-catch block, we set loading state to false. Finally, we return isLoading, success, users, errorMessage, and getUsers so that they can be used whenever we call the useFetchUser Hook anywhere in the application.

Setting up the loader component

The loader component will be used to indicate when the API fetching is in progress. To build the loader, we need to import necessary dependencies and then pass the isLoading and withText parameters. The most important dependency is the ActivityIndicator which handles the actual loading indicator.

Here’s the code for our loader component:

// JavaScript
// components/Loader.jsx
import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';

export const Loader = ({ isLoading = false, withText = false }) => {
  return isLoading ? (
    <View style={styles.loader}>
      <ActivityIndicator size='large' color='#aaaaaa' />
      {withText ? (
        <Text style={{ color: 'green' }}>Loading users...</Text>
      ) : null}
    </View>
  ) : null;
};
const styles = StyleSheet.create({
  loader: {
    marginVertical: 15,
    alignItems: 'center',
  },
});

To style the loader, we simply added a vertical margin and centered the components.

Building the item component

The Item.jsx component is used to represent the rendered item of the FlatList components that we’ll build. The Item function accepts item and index parameters and returns a View with an Image and a second View component.

The second View component returns two Text components that we use to display the name and email from the item parameter. The Image component displays a large picture also from the item parameter.



Here’s the code for our Item component:

// JavaScript
// components/Item.jsx
import { StyleSheet, View, Text, Image } from 'react-native';

export const Item = ({ item, index }) => {
  return (
    <View style={styles.itemWrapper}>
      <Image source={{ uri: item?.picture?.large }} style={styles.itemImage} />
      <View style={styles.itemContentWrapper}>
        <Text
          style={styles.itemName}
        >{`${item?.name?.title} ${item?.name?.first} ${item?.name?.last}`}</Text>
        <Text style={styles.itemEmail}>{`${item?.email}`}</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  itemWrapper: {
    flexDirection: 'row',
    paddingHorizontal: 16,
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderColor: '#dddddd',
  },
  itemImage: {
    width: 51,
    height: 51,
    marginRight: 15,
  },
  itemContentWrapper: {
    justifyContent: 'space-around',
  },
  itemName: {
    fontSize: 17,
  },
  itemEmail: {
    color: '#777777',
  },
});

Setting up pull-to-refresh functionality

We’ll create a TopLoader.jsx screen to demonstrate to the user how to handle pull-to-refresh at the top of the list. As a first step in building this screen, we import necessary dependencies, including the FETCH_RESULTS, useFetchUser.js, Item.jsx, and Loader.jsx files:

// JavaScript
// screens/TopLoader.jsx

import { StyleSheet, Text, View, FlatList, RefreshControl } from 'react-native';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';

Next, we create the component and initialize the states:

// JavaScript
// screens/TopLoader.jsx

const TopLoader = () => {
  const flatListRef = useRef(null);
  const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);

  return (
    <View>

    </View>
  );
};
export default memo(TopLoader);

Here, we define a flatListRef that we use to control automatic scrolling. We also call the useFetchUser Hook and destructure the following data from it: isLoading, success, users, errorMessage, and getUsers.

Next, we define the state to handle the current page, which we pass to the getUsers function, and also the refreshing state that will control the visibility of the pull-to-refresh loader.

We create a function to increment the current page state and another function to update the refreshing state:

// JavaScript
// screens/TopLoader.jsx

  const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    loadMoreItem();
  }, []);

In the below code, we create a scrollToItem function that serves as a crucial addition to our FlatList. This function automatically handles scrolling when our data changes:

// JavaScript
// screens/TopLoader.jsx

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

The scrollToItem function is very important because when new data is added at the beginning of the FlatList, React Native will attempt to display the initial set of items. This behavior can be perplexing for users, who would naturally anticipate continuing their scrolling from the point at which they refreshed the content.

Now, we simply check for a successful API request and then set the refreshing state to true. We also call the scrollToItem function and pass the FETCH_RESULTS - 1 as the index parameter:

// JavaScript
// screens/TopLoader.jsx

  useEffect(() => {
    if (success) {
      setRefreshing(false);
      scrollToItem(FETCH_RESULTS - 1);
    }
  }, [success]);

We can also add code to handle the API call on initial page load and whenever the current page changes:

// JavaScript
// screens/TopLoader.jsx

  useEffect(() => {
    getUsers(currentPage, true);
  }, [currentPage]);

As you may have observed while using the useEffect Hook, we included additional values in an array known as dependencies. These dependencies play a crucial role in how the useEffect Hook operates.

If we omit the dependency array, our code will execute with every re-render, and if we supply an empty array as the dependency the useEffect Hook will run only once. However, when we specify particular values within the dependency array, the Hook will trigger during the initial render and whenever the values of those dependencies change.

Now it’s time to add our jsx markup. Begin by adding the following skeleton:

// JavaScript
// screens/TopLoader.jsx

    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Pull To Refresh Control
      </Text>
      <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
      />
      {errorMessage ? <Text>{errorMessage}</Text> : null}
    </View>

In this markup, we simply have a Text component, a FlatList, and a second Text component that is used to render any error message that may have been passed from the useFetchuser Hook.

Our FlatList component accepts a ref that we previously defined above. It also accepts data, in the form of users from useFetchUser Hook, a renderItem which is the Item component, and then the keyExtractor, which is the email of each user.

In order to measure our progress, we‘ll need to render our TopLoader on the screen. To do so, add the following code to the App.js file:

// JavaScript
// App.js

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import TopLoader from './screens/TopLoader';

const App = () => {
  return (
    <View style={styles.container}>
      <TopLoader />
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

Now we can open the terminal and run the npm run start command to start up the server and then press a or i to run the app on Android or iOS, respectively.


More great articles from LogRocket:


Next, let’s go back to the TopLoader.jsx component and add the refreshControl prop to the FlatList component:

// JavaScript
// screens/TopLoader.jsx

      <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
      />

The RefreshControl component is provided by the React Native team to handle pull-to-refresh at the top of a FlatList, ScrollView, or ListView. This component accepts a couple of props, but only the refreshing props is required to make it work. Therefore, we need to pass the refreshing state we defined above to this component.

The next prop is onRefresh which is called when refreshing has begun. For this project, we’ll pass the onRefresh function we created above so that when we refresh, the onRefresh callback will trigger the getUsers function in the useEffect Hook to run.

If we test our application’s pull-to-refresh functionality at this point, we’ll see an error: “Layout doesn’t know the exact location of the requested element. Falling back to calculating the destination manually.” To address this issue, let’s add the onScrollToIndexFailed prop to our FlatList component, like so:

// JavaScript
// screens/TopLoader.jsx

 <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />

Our pull-to-refresh functionality should be working seamlessly now, but we can further improve it by adding three additional props: initialScrollIndex, maxToRenderPerBatch, and ListEmptyComponent:

// JavaScript
// screens/TopLoader.jsx

 <FlatList
        ref={flatListRef}
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />

N.B., the ListEmptyComponent is used to render any component when the list is empty; in our case, we have provided a Loader component

Customizing the refresh control

You can customize a refresh control by providing certain properties. These properties allow you to modify the background color, text color, and other aspects of the loader offered by the component.

You can find a list of props that can be passed to the refresh control component here, but it’s important to bear in mind that certain properties will function exclusively on Android, while others will only work on iOS. For instance, the colors prop accepts a list of colors and will only work on Android, while the tintColor prop works only on iOS.

To provide a custom color for the refresh control for all use cases, you’ll need to combine both props:

// JavaScript
// screens/TopLoader.jsx

 <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            tintColor='red'
            colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
          />

Our application’s top loader screen with pull-to-refresh functionality and a new, custom refresh control is shown below:

React Native App Pull-To-Refresh Functionality Custom Refresh Control
React Native app with pull-to-refresh functionality and custom refresh control.

Setting up scroll-to-refresh functionality

The concept behind scroll-to-refresh is that when users reach the bottom of a list and continue dragging the list downward, it will automatically update by fetching the next set of results. To achieve this functionality, we use two specific props for the FlatList component: onEndReached and onEndReachedThreshold.

The onEndReached prop triggers when the user reaches a certain distance from the end of the list, as defined by the onEndReachedThreshold prop. This threshold value determines how far, in terms of visibility, the bottom edge of the list should be from the end of its content. When the threshold is set to zero, the onEndReached callback will be activated as soon as the user scrolls to the very bottom of the list.

For demonstration purposes, let’s open components/BottomLoader.jsx file and add the following code:

// JavaScript
// screens/BottomLoader.jsx

import { FlatList, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';


const BottomLoader = () => {
  const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
  const [currentPage, setCurrentPage] = useState(1);

  const loadMoreItem = (e) => {
    setCurrentPage((prev) => prev + 1);
  };

  useEffect(() => {
    getUsers(currentPage);
  }, [currentPage]);

  return (
    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Infinite Bottom Loader
      </Text>
      <FlatList
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
      />
      {errorMessage ? <Text>{errorMessage}</Text> : null}
    </View>
  );
};
export default BottomLoader;

In the above code, we import the required dependencies and initiate the useFetchUser Hook to retrieve data. Then we establish the current page state and create a function called loadMoreItem, that increments the currentPage state.

The useEffect Hook observes changes in state of the current page and triggers the getUsers callback. We also include similar markup to what we provided for the TopLoader.

To implement the scroll-to-refresh functionality, we simply update our FlatList component with the below code:

// JavaScript
// screens/BottomLoader.jsx

     <FlatList
        data={users}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        ListFooterComponent={<Loader isLoading={isLoading} />}
        onEndReached={loadMoreItem}
        onEndReachedThreshold={0}
        maxToRenderPerBatch={FETCH_RESULTS}
        ListEmptyComponent={<Loader isLoading />}
      />

N.B., in the above code the ListFooterComponent is used to indicate the loading state of the API requests, but can be any custom component of your choice

To test out what we’ve built, update the App.js file, like so:

// JavaScript
// App.js

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import BottomLoader from './screens/BottomLoader';

const App = () => {
  return (
    <View style={styles.container}>
      <BottomLoader />
    </View>
  );
};
export default App;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

Here’s our application, showing the scroll-to-refresh functionality in action:

React Native App Scroll-To-Refresh Functionality
React Native app with scroll-to-refresh functionality.

Combining the refresh functionalities

Next, we’ll streamline the pull-to-refresh functionality and scroll-to-refresh functionality into a single FlatList component.

To start, open the screens/Combined.jsx file and import the required dependencies and components:

// JavaScript
// screens/Combined.jsx

import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';

Next, we create our component and call the useFetchUser Hook in two places for the top and bottom loaders, like so:

// JavaScript
// screens/Combined.jsx

const Combined = () => { 
const flatListRef = useRef(null);

  const {
    isLoading: isLoadingTop,
    success: successTop,
    users: usersTop,
    errorMessage: errorMessageTop,
    getUsers: getUsersTop,
  } = useFetchUser();
  const {
    isLoading: isLoadingBottom,
    success: successBottom,
    users: usersBottom,
    errorMessage: errorMessageBottom,
    getUsers: getUsersBottom,
  } = useFetchUser();

return (
  <View>

  </View>
)

export default Combined;

In JavaScript, we can rename a destructured value by using a colon after the value:

const { isLoading } = useFetchUser();

// isLoading can be renamed to isLoadingTop
const { isLoading: isLoadingTop } = useFetchUser();

Now, we define the combinedUsers, currentPage, refreshing, and isTop states:

// JavaScript
// screens/Combined.jsx

  const [combinedUsers, setConfirmedUsers] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);
  const [isTop, setIsTop] = useState(false);

Then we define the loadMoreItem function:

// JavaScript
// screens/Combined.jsx  
const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

Next, we define the onRefresh callback and scrollToItem function:

// JavaScript
// screens/Combined.jsx  

const onRefresh = useCallback(() => {
    setRefreshing(true);
    setIsTop(true);
    loadMoreItem();
  }, []);

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

Then, we define the useEffect Hook to fetch the data:

// JavaScript
// screens/Combined.jsx  

  useEffect(() => {
    if (isTop) {
      getUsersTop(currentPage, isTop);
    } else {
      getUsersBottom(currentPage, isTop);
    }
  }, [currentPage]);

In the above code, we check for when the value of isTop changes in order to determine the function to call each time the currentPage state changes.

Then we define actions for when the pull-to-refresh and scroll-to-bottom API requests are successful:

// JavaScript
// screens/Combined.jsx  

useEffect(() => {
    if (successTop) {
      setRefreshing(false);
      setConfirmedUsers(usersTop);
      if (combinedUsers.length > 0) {
        scrollToItem(FETCH_RESULTS - 1);
      }
    }
  }, [successTop]);

  useEffect(() => {
    if (successBottom) {
      setConfirmedUsers(usersBottom);
    }
  }, [successBottom]);

Next, we combine the markup, like so:

// JavaScript
// screens/Combined.jsx    

  <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Combined Bidirectional FlatList
      </Text>
      {errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
      <FlatList
        ref={flatListRef}
        data={combinedUsers}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            // tintColor='red'
            // colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
            // tintColor='transparent'
            // colors={['transparent']}
            // style={{ backgroundColor: 'transparent' }}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        // ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
        ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
        onEndReached={() => {
          loadMoreItem();
          setIsTop(false);
        }}
        onEndReachedThreshold={0}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />
      {errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
    </View>

Our component should look like this:

// JavaScript
// screens/Combined.jsx 

import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
const Combined = () => {
  const flatListRef = useRef(null);
  const {
    isLoading: isLoadingTop,
    success: successTop,
    users: usersTop,
    errorMessage: errorMessageTop,
    getUsers: getUsersTop,
  } = useFetchUser();
  const {
    isLoading: isLoadingBottom,
    success: successBottom,
    users: usersBottom,
    errorMessage: errorMessageBottom,
    getUsers: getUsersBottom,
  } = useFetchUser();

  const [combinedUsers, setConfirmedUsers] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [refreshing, setRefreshing] = useState(false);
  const [isTop, setIsTop] = useState(false);

  const loadMoreItem = () => {
    setCurrentPage((prev) => prev + 1);
  };

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    setIsTop(true);
    loadMoreItem();
  }, []);

  const scrollToItem = (index) => {
    flatListRef.current.scrollToIndex({ index: index, animated: false });
  };

  useEffect(() => {
    if (isTop) {
      getUsersTop(currentPage, isTop);
    } else {
      getUsersBottom(currentPage, isTop);
    }
  }, [currentPage]);

  useEffect(() => {
    if (successTop) {
      setRefreshing(false);
      setConfirmedUsers(usersTop);
      if (combinedUsers.length > 0) {
        scrollToItem(FETCH_RESULTS - 1);
      }
    }
  }, [successTop]);

  useEffect(() => {
    if (successBottom) {
      setConfirmedUsers(usersBottom);
    }
  }, [successBottom]);

  return (
    <View>
      <Text
        style={{
          textAlign: 'center',
          paddingVertical: 10,
          fontSize: 18,
          fontWeight: '600',
        }}
      >
        Combined Bidirectional FlatList
      </Text>
      {errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
      <FlatList
        ref={flatListRef}
        data={combinedUsers}
        renderItem={Item}
        keyExtractor={(item) => item?.email}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            // tintColor='red'
            // colors={['red', 'green', 'blue']}
            // progressBackgroundColor={'green'}
            // title='Loading users...'
            // titleColor='green'
            // size={'large'}
            // progressViewOffset={200}
            // tintColor='transparent'
            // colors={['transparent']}
            // style={{ backgroundColor: 'transparent' }}
          />
        }
        maxToRenderPerBatch={FETCH_RESULTS}
        // ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
        ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
        onEndReached={() => {
          loadMoreItem();
          setIsTop(false);
        }}
        onEndReachedThreshold={0}
        ListEmptyComponent={<Loader isLoading />}
        initialScrollIndex={0}
        // Layout doesn't know the exact location of the requested element.
        // Falling back to calculating the destination manually
        onScrollToIndexFailed={({ index, averageItemLength }) => {
          flatListRef.current?.scrollToOffset({
            offset: index * averageItemLength,
            animated: true,
          });
        }}
      />
      {errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
    </View>
  );
};
export default Combined;
const styles = StyleSheet.create({});

To see what we’ve built, update the App.js file, like so:

// JavaScript
// App.js  

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Combined from './screens/Combined';

const App = () => {
  return (
    <View style={styles.container}>
      <Combined />
    </View>
  );
};
export default App;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 40,
    marginHorizontal: 20,
  },
});

Our final application should function like this:

React Native App Pull-To-Refresh Scroll-To-Refresh Functionality
React Native application with pull-to-refresh and scroll-to-refresh functionality.

Conclusion

In the article, we investigated how to implement pull-to-refresh and scroll-to-refresh and also how to combine these functionalities in a React Native project. We also discussed how to customize the refresh control component.

Thanks for reading! I hope you found this article useful for better understanding how to add pull-to-refresh and refresh control to your React Native application. Be sure to leave a comment if you have any questions. Happy coding!

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — try LogRocket for free.

Onuorah Bonaventure Full-stack web developer that loves connecting brands to their customers by developing amazing, performant websites. Interested in teaching programming concepts and breaking down difficult concepts. When I'm not coding, I play chess and checkers with my friends while listening to cool Afrobeats.

Leave a Reply