React Native Lists: Load More by Scrolling

How to progressively load more list items as you scroll in React Native

This piece demonstrates how to load more items in a FlatList component as you scroll down, as well as how to persist lists on device storage, and keep those lists in sync as more items are loaded.

The demo accompanying this piece (available here on GitHub) demonstrates what we’ll achieve, with more items loading from an external data source as the list is being scrolled down. Not only this, the list is persisted on device and in state as more items are loaded.

The following screencast summarises the process. Note that when we start scrolling more results begin to load immediately. In addition, more results are continuously loaded if the end of the list has been reached. This is due to the onEndReachedThreshold prop of FlatList, that triggers an event to load more items if the scroll position is within this threshold. This behaviour can be seen here:

Loading more items while scrolling

Notice when the list starts very small (5 items in the above example), the end of the list is on screen from the start. This triggers a load more event as soon as the user starts scrolling.

We’ll be utilising some supported props of FlatList to display a header that reflects the total amount of items in a list, and a footer that displays a text when more items are being loaded. Importantly, FlatList comes with event listeners out of the box that can be utilised to trigger a load more function.

Before exploring the JavaScript, let’s briefly recap how load more functionality can benefit your app and user experience as a whole.

Why Loading More is Important

The need for a load more function in your apps become very apparent when lists begin getting long, whether that be displaying product lists, saved items, browsing history, or any other type of list that your app may need to display. Fetching longer lists of records becomes more problematic when they take a while to aggregate server-side, leaving the user waiting a prolonged period before the list is returned and rendered in-app.

A common place to populate list content is when a user signs in to their account. At this stage, user settings and saved items relating to the user account are populated into the app’s Dashboard and on-device data storage. For example, the following illustration considers some of the lists needed for an eCommerce application:

Pre-populating app list content as a user signs in.

This initial setup can be a time consuming process depending on how much content needs to be fetched and how that data is aggregated server-side. To prevent this initial account setup from taking too long, such lists need to be limited, but without sacrificing the user experience or removing detail from each list item.

This is concretely why a load more function is useful — it allows us to pre-populate lists quickly with a small amount of items, say 5 to 10 records, but leaves the door open to fetch more in the event the user is interested in browsing them.

Apps feel a lot more responsive when lists are limited to a few initial items. The user’s interest to view more items is then signalled rather naturally by scrolling down the list, and this is when additional requests to load more items should be made. This is the scenario we are after, and React Native provides some very useful props that allow us to do just this.

Initially fetching a small amount of list items is also good app design. There is no guarantee the user will even browse the fetched lists. Therefore, fully populating lists with the expense of longer loading times is a trade-off worth avoiding.

Beyond limiting initial list items, there is more we can do to speed up list loading in future sessions, in the form of saving lists of AsyncStorage and global Redux state.

Persisting lists with AsyncStorage

What we’ll be doing further down is saving fetched lists in AsyncStorage and update Redux store state concurrently. Redux makes it easy for us to fetch state using useSelector , whereas AsyncStorage will persist such data to the device so it will stay around even after the user closes the app. Upon subsequent app sessions, the lists will still be present, avoiding another fetch request to populate them again.

To learn more about Redux and Redux hooks in-particular, I have published a dedicated piece on the subject: Redux Hooks in React: An Introduction.

Both sources need to be kept in sync as more list items are loaded as the user is scrolling. In addition, Redux state should be pre-populated from AsyncStorage when an app is initially opened. If there are no saved lists in AsyncStorage, then the app can go ahead and make a fetch request. The following illustration summarises this data flow:

More results are fetched as the user scrolls down.

This is the scenario this piece implements using FlatList — we’ll synchronise both the AsyncStorage data source and Redux store simultaneously every time more list items are fetched via a loading more function.

To summarise this section, fetching an initially small amount of items and persisting lists on device storage is good design, because:

  • Faster loading times will result from fetching smaller lists, which will in-turn require less bandwidth and an overall better user experience.
  • Your lists will not need to be populated again after the initial fetch, even upon subsequent app visits — they’ll be in on-device storage.

Let’s next explore the List component that has been implemented and to create a working load more function.

List Component Setup

From here we will be discussing the List.js component of the accompanying project (on GitHub), and how the component has been configured to load additional items and manage state in the way discussed above. This component houses the FlatList and the accompanying functions to get load more working.

Let’s first understand how data is fed into the list.

Feeding items into FlatList

The data being fed into the FlatList is stored in a separate data.js file that contains 50 records, each of which having a unique _id identifier. This file mimics the behaviour of an API request that will be made in production, but for testing purposes the data has been stored locally.

Here is a snippet of the data object:

// structure of list dataexport const data = [
{
_id: 1,
},
{
_id: 2,
},
...
];

This is actually all the information a FlatList needs to display items — a unique identifier to use with the keyExtractor prop. In this case the _id property of each item is used as the key.

This bare-bones setup was intentional so the reader can adapt the project to their own use cases, with more complex objects. An _id field is also a popular convention for most database systems.

To mimic a RESTful API request, data.js has another export in the form of the fetchResults(startingId) function. The function will fetch more results starting from a particular _id value. When the list is first initialised, fetchResults(0) will be called to fetch the first 5 list items.

Here is fetchResults in its entirety:

// fetchResults takes records from `data` from a particular `_id`const RECORDS_PER_FETCH = 5;export const fetchResults = (startingId = 0) => {
let obj = [];
// loop through records starting at `startingId`
for (
let i = startingId;
i < startingId + RECORDS_PER_FETCH;
i++) {
// break loop if list comes to an end
if (data[i] === undefined)
break;
// add record to `obj`
obj.push(data[i]);
}
return obj;
}

Notice the RECORDS_PER_FETCH constant that determines how many records are fetched on each load more event. 5 has been set for demonstration purposes, but try to assign a value that will fill the entire device screen on the initial load to prevent immediate subsequent load more requests.

Now let’s take a look at initialising this data inside the List component.

Initialising data from Redux store, then AsyncStorage

It is important to note that initially, we check whether any list data is already saved in either the Redux store and AsyncStorage.

In the event Redux is already in sync, dispatching the action above at the initialisation stage will not result in a re-render as no data changes would be made. Therefore, there are no performance trade-offs by always running this initialisation function, that ensures Redux is in sync with AsyncStorage.

The following illustration summarises the entire initialisation process:

Initialising a list from Redux and AsyncStorage, with AsyncStorage the true source of data.

Breaking down this process, we first try to fetch the Redux record using the useSelector hook:

const listItems = useSelector(state => state.list.items);

List items are stored in a list reducer under an items property.

This is the object that is initially fed into FlatList. If listItems is null, then no items will be displayed. We then go ahead and carry out the initialisation logic to check if the Redux store is in sync with AsyncStorage via an initialiseList() function.

This initialisation is triggered in a useEffect hook after the first component render:

// call initialisation function with useEffectuseEffect(() => {
initialiseList();
}, []);
const initialiseList = async () => {
...
}

From here we can delve into initialistList(). Here is the function in full with each line commented:

// `initialiseList` function in fullconst initialiseList = async () => {   // [for testing purposes] reset AsyncStorage on every app refresh
await AsyncStorage.removeItem('saved_list');
// get current persisted list items (will be null if above line is not removed)
const curItems = await AsyncStorage.getItem('saved_list');
if (curItems === null) {
// no current items in AsyncStorage - fetch initial items
json = fetchResults(0);
// set initial list in AsyncStorage
await AsyncStorage.setItem('saved_list', JSON.stringify(json));
} else {
// current items exist - format as a JSON object
json = JSON.parse(curItems);
}
// update Redux store (Redux will ignore if `json` is same as current list items)
dispatch({
type: 'UPDATE_LIST_RESULTS',
items: json
});
}

This logic will efficiently set up your list with the initial items in place. Let’s next visit the key FlatList props needed to execute the load more functionality, and finally implement it.

FlatList Component Props

This section breaks down the props used in FlatList, but the reader can refer to the entire implementation here on GitHub.

The data and key extractor have been configured based on listItems, the data derived from Redux:

<FlatList
...
data={listItems}
keyExtractor={(item) => "item_" + item._id}
/>

A threshold has been set to determine when to trigger the load more event — or how far from the end of the list a user has to be before we trigger load more. This is done with the onEndReachedThreshold prop, that triggers the onEndReached event when that threshold has been met:

<FlatList
...
onEndReachedThreshold={0.01}
onEndReached={info => {
loadMoreResults(info);
}}
/>

A threshold of 0.01 forces the user to scroll right to the bottom of the list before onEndReached is triggered. This is not ideal in production as you will want the additional items to already be in place once the user arrives at the previous bottom list position. Experiment with this value to get the right balance.

Once onEndReached is triggered, the loadMoreResults() function is called. The info argument pertains to metadata about the onEndReached event; it is unneeded here but can be useful for debugging purposes.

Other notable props are ListHeaderComponent and ListFooterComponent that allow us to display a title and load more label respectively.

Now let’s turn our attention to loadMoreResults(), the function responsible to orchestrating the load more logic itself.

Loading More Results

The loadMoreResults() function is designed to mimic an API request and return the additional list items. It relies on some local component state to keep track of whether the component is in the middle of fetching results (as not to duplicate a request), and whether all list items are loaded.

The full implementation of loadMoreResults() is as follows:

// `loadMoreResults` function in fullconst loadMoreResults = async info => {   // if already loading more, or all loaded, return
if (loadingMore || allLoaded)
return
// set loading more (also updates footer text)
setLoadingMore(true);
// get next results
const newItems = fetchResults(totalItems);
// mimic server-side API request and delay execution for 1 second
await delay(1000);
if (newItems.length === 0) {

// if no new items were fetched, set all loaded to true to prevent further requests
setAllLoaded(true);

} else {
// process the newly fetched items
await persistResults(newItems);

}
// load more complete, set loading more to false
setLoadingMore(false);
}

Note that this function only executes the process of fetching more results. Those results are then passed into the final function of this implementation, persistResults().

persistResults() appends the newly fetched list items to the current list items, then updates our two data sources — Redux and AsyncStorage:

// `persistResults` function in fullconst persistResults = async (newItems) => {   // get current persisted list items
const curItems = await AsyncStorage.getItem('saved_list');
// format as a JSON object
let json = curItems === null
? {}
: JSON.parse(curItems);
// add new items to json object
for (let item of newItems) {
json.push(item);
}
// persist updated item list
await AsyncStorage.setItem('saved_list', JSON.stringify(json));
// update Redux store
dispatch({
type: 'UPDATE_LIST_RESULTS',
items: json
});
}

Calling the Redux dispatch at the end of this function will trigger a re-render, and thus update the list with the additional items present. And this is the entire load more process completed!

By utilising React Native building blocks in conjunction with state management, the advantages of the article discussed at the beginning of this article can be leveraged to offer a more elegant and streamlined user experience.

In Summary

This piece visited an efficient method of loading more data into a FlatList. by leveraging multiple data persisting methods, namely Redux and Async Storage.

We’ve covered how to keep the two sources in sync by firstly manipulating the AsyncStorage value, then passing that updated value to Redux. Notice that we only need to manipulate one data source before updating the second one with that updated value.

Again, the demo of this piece is available on GitHub in an Expo managed app where the reader can simply clone or fork the project for their own purposes.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store