Adding value when you reach the start of a flatlist — the one-minute setup

Bouteiller < A2N > Alan
7 min readJul 18, 2023

--

This story is related to another story, talking about making a horizontal calendar with flatlist, you can find this story just here.
If you don’t want to add data at the start but at the end,
check the answer here.

Ok, so you want to add data at the beginning of your flatlist ?

We have a problem right now: flatlist provides a parameter to detect when the user has reached the end, but not to detect when the user has reached the beginning!

For this particular problem we have, hopefully, a workaround !

But first we need to build a simple flatlist.

Disclaimer

For this tutorial purpose :

  • I assume you know what a flatlist is
  • I used native-base for the style, but the whole thing works the same with React Native (https://nativebase.io/)

Setting up a very basic flatlist

So, for starting, let’s create a simple flatlist showing number from 0 to 5.

The code look like this :

import React, { type ReactElement, useState } from 'react';
import { Center, FlatList } from 'native-base';
import { type ListRenderItemInfo, type ScaledSize, Dimensions } from 'react-native';

const FlatListDemo = (): ReactElement => {
const windowDimensions: ScaledSize = Dimensions.get('window');
const [data] = useState<number[]>([0, 1, 2, 3, 4, 5]);

/**
* render the item for each flatlist element
* @param {number} item just the number
* @param {number} index index of the element
* @returns {ReactElement} native base center element with the number in it
*/
function renderItem(item: number, index: number): ReactElement {
return <Center
backgroundColor={`blue.${index+1}00`}
width={windowDimensions.width}
>
{item.toString()}
</Center>;
}

return (
<FlatList
data={data}
horizontal={true}
snapToInterval={windowDimensions.width}
keyExtractor={(item: number, index: number): string => index.toString()}
renderItem={(itemInfo: ListRenderItemInfo<number>) => {
return renderItem(itemInfo.item, itemInfo.index);
}}
/>
);
};

export default FlatListDemo;

Nothing much special, we define a flatlist with some data and set it up to a certain width for having the list with a scrollbar.

The snapToInterval is given the same value as the element width. This tells the flatlist that we want the user to scroll one element at a time.

We can swipe !

Adding data when the user reaches the start

Ok, I promise you a workaround, so here we go : the onScroll parameter

// use `onScroll` to handle the data when the user reach the start
onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>): void => { handleScroll(event); }}

The onScroll event gives us the ability to read the real value of the scroll offset, with this value we just have a little math to do and, voilà, we have our “detector” 🙃 :

/**
* add data to the start of the flatlist
* @param {NativeSyntheticEvent<NativeScrollEvent>} event scroll event
*/
function prependData(event: NativeSyntheticEvent<NativeScrollEvent>): void {
// if distanceFromStart.x === 0 we reach the start of the list
const distanceFromStart: NativeScrollPoint = event.nativeEvent.contentOffset;
if (distanceFromStart.x === 0) {
console.log('prepend !');
}
}

For now, the code look like that :

import React, { type ReactElement, useState } from 'react';
import { Center, FlatList } from 'native-base';
import {
type ListRenderItemInfo,
type ScaledSize,
Dimensions,
type NativeSyntheticEvent,
type NativeScrollEvent, type NativeScrollPoint
} from 'react-native';

const FlatListDemo = (): ReactElement => {
const windowDimensions: ScaledSize = Dimensions.get('window');
const [data, setData] = useState<number[]>([0, 1, 2, 3, 4, 5]);

/**
* render the item for each flatlist element
* @param {number} item just the number
* @param {number} index index of the element
* @returns {ReactElement} native base center element with the number in it
*/
function renderItem(item: number, index: number): ReactElement {
return <Center
backgroundColor={`blue.${index+1}00`}
width={windowDimensions.width}
>
{item.toString()}
</Center>;
}

/**
* add data to the start of the flatlist
* @param {NativeSyntheticEvent<NativeScrollEvent>} event scroll event
*/
function prependData(event: NativeSyntheticEvent<NativeScrollEvent>): void {
// if distanceFromStart.x === 0 we reach the start of the list
const distanceFromStart: NativeScrollPoint = event.nativeEvent.contentOffset;
if (distanceFromStart.x === 0) {
console.log('prepend !');
}
}

return (
<FlatList
data={data}
horizontal={true}
snapToInterval={windowDimensions.width}
keyExtractor={(item: number, index: number): string => index.toString()}
renderItem={(itemInfo: ListRenderItemInfo<number>) => {
return renderItem(itemInfo.item, itemInfo.index);
}}
onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>): void => { prependData(event); }}
/>
);
};

export default FlatListDemo;

The result :

That work ! :)

Ok, now we can add data to your list. Like so :

if (distanceFromStart.x === 0) {
const d = [[20, 21, 22, 23, 24, 25], data];
setData(d.flat());
}

I have prefixed the added data with 2 because here, unfortunately, we have a new problem !

As you can see in the gif, the index where the user is located is not preserved!
The result is an odd interaction for the user. We need to correct this.

Solving the index problem

Our problem is pretty straight forward : when the data is added the size of the list changed, that mean for the same index we have another data. The flatlist doesn’t move the user to the same index, but the data changed for the user.

We need to store where the user is “looking”. For that, we need to use the following parameter :

// 50 means that item is considered visible if it's visible for more than 50 percents
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
onViewableItemsChanged={onViewableItemsChanged.current}

Here onViewableItemsChanged is a reference :

const onViewableItemsChanged = useRef((info: onViewableItemsChangedInfo): void => { setCurrentObjectInfo(info); });
const [currentObjectInfo, setCurrentObjectInfo] = useState<onViewableItemsChangedInfo | undefined>(undefined);

We also need to define the getItemLayout parameter, this is mandatory if you want the onViewableItemsChanged working correctly :

getItemLayout={(data: number[] | null | undefined, index: number): { length: number, offset: number, index: number } => ({
length: windowDimensions.width, offset: windowDimensions.width * index, index
})}

Ok […] let's summarize a little bit here :

  • we have created a reference for the event when the item showed by the flatlist change called onViewableItemsChanged
  • we have mapped a state to the onViewableItemsChanged reference like so when the item changed the state is updated
  • we have defined the trigger point for considering an item visible to 50% of its own width
  • we have implemented the getItemLayout parameter

By the way, the onViewableItemsChangedInfo type is just :

export interface onViewableItemsChangedInfo {
viewableItems: ViewToken[]
changed: ViewToken[]
}

If we create a little useEffect we can check that our setup work fine :

useEffect(() => { console.log('the data length is', data.length); }, [data]);
useEffect(() => { console.log('the index of the visible item is', currentObjectInfo?.viewableItems[0].index); }, [currentObjectInfo?.viewableItems[0].index]);
Ok-We have the index!

Now that we have the index, we need to add the length of the new data to that index to get the new index.

if (distanceFromStart.x === 0) {
const newData = [20, 21, 22, 23, 24, 25];

let indexToMove: number = 0;
if (typeof currentObjectInfo?.viewableItems[0]?.index === 'number') {
indexToMove = currentObjectInfo?.viewableItems[0].index;
indexToMove = indexToMove + newData.length;
}

setData([...newData, ...data]);
}

Because we have a known array in newData it’s easier to test that our solution work.

We have an array of 6, and we add 6 new elements, when the data is added we are at the index 0 of the first array so when the new data append we need to go to the new data length + the old index.

In our case it’s always the same (6), but if you do an API call, for example, you don’t know the length of your data in advance.

Now that we have the new index, we just have to move the user to this index.

We need to use the scrollToIndex function of the flatlist. For that, we have to use a reference to the flatlist :

const demoFlatList = useRef();
<FlatList
ref={demoFlatList}
...

Now we just have to give the right index to the function :

/**
* set the given index for the flatlist
* @param {number} index the index you want the flatlist to be
* @param {boolean} isAnimated if you want an animated transition
*/
function goToIndex(index: number, isAnimated: boolean): void {
if (demoFlatList.current !== undefined) {
demoFlatList.current.scrollToIndex({ index, animated: isAnimated });
}
}

The animation value is set to false because we want the scrolling to be invisible to the user.

if (distanceFromStart.x === 0) {
const newData = [20, 21, 22, 23, 24, 25];

let indexToMove: number = 0;
if (typeof currentObjectInfo?.viewableItems[0]?.index === 'number') {
indexToMove = currentObjectInfo?.viewableItems[0].index;
indexToMove = indexToMove + newData.length;
}

setData([...newData, ...data]);
goToIndex(indexToMove, false);
}
Here we go :)

And well done folks ! You have data at the beginning of your flatlist !

--

--