React Native FlatList — A Comprehensive guide — Part 2

Gautham Vijayan
9 min readAug 3, 2024

Gautham Vijayan | Lead React Native Engineer

This post is a follow-up to my previous post Part I.

In this post, we will be looking into the following features, FlatList offers us and as a working example, I will showcase how you as a developer can integrate Instagram Style Status Circles inside your application with all the concepts discussed in the previous post as well as in this one.

Here is the list of features:

  1. How we can scroll to a particular element in the FlatList?
  2. Implementing Animated.FlatList for neat layout transitions.
  3. Utilising numColumns prop to make multi column layout.
  4. Let’s make Instagram Styled Status in FlatList.
  5. Bonus : Handling Failed scrollToIndex Condition.

ScrollToIndex Function

We would have seen many applications use the feature where when we tap on a card element, the focus gets shifted to that element with a nice animation.

In order to integrate this feature in React Native, we can use FlatList’s scrollToIndex function with help of “Ref” from React JS.

For the first step, we need to attach the ref to the FlatList.

Once attached, the ref can be used to scroll to a particular element in the data array.

When the user taps on the element we will be grabbing that particular element’s index and passing on to the scrollToIndex function so that the viewport gets moved to that element.

We will be using the animated prop inside the scrollToIndex function so that when the user taps on the element it animates to that element rather than moving abruptly to the element which will not provide proper UX.

Here is the full code for the implementation


const data = [
{
title: 'Formentera Ship',
authorName: 'Roberto H',
},

{
title: 'Sunset Ship',
authorName: 'Bruce Warrington',
},

{
title: 'Green Boats',
authorName: 'Ian Badenhorst',
},
];

const flatListRef = useRef(); // import useRef from React

function moveForward(position) {
flatListRef.current.scrollToIndex({index: position, animated: true});
}

const RefComponent = ({item, index}) => {
function move() {
moveForward(index);
}
return (
<Pressable onPress={move}>
<View>
<Text>{item?.title}</Text>
</View>
</Pressable>
);
};

<FlatList
data={data}
ref={flatListRef}
renderItem={RefComponent}
keyExtractor={item => item?.title}
showsVerticalScrollIndicator={false}
/>;

MicroInteraction with Animated.FlatList

Let’s take you want to integrate a feature where you want to search the products which is available in your store like Amazon or Walmart, you will be utilising a Search Bar and filtering the list of elements in the FlatList. (Assuming its just Frontend because if its backend you will be using your own logic like Fulltext search etc.).

When the user starts searching, he will look into the jumping of elements vertically without any layout animation which may be jarring.

To tackle this issue you can use Layout Animations with help of the Animated prop provided by react-native-reanimated libary and the LinearTransition prop as well which we can customise.

So install the react-native-reanimated library as well as the gesture handler library before moving forward. The gesture handler library’s GestureHandlerRootView should be used here.

npm i react-native-reanimated react-native-gesture-handler

This will make the filtering of the elements to behave smoothly which will in turn improve UX.

I use react-native-fast-image for image caching. You can use normal Image prop from React native aswell.

You can find the code below as well as working video.


import {Dimensions, SafeAreaView, StyleSheet, Text, View, TextInput} from 'react-native';
import React, {useState} from 'react';
import Animated, {LinearTransition} from 'react-native-reanimated';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import FastImage from 'react-native-fast-image';

const screenWidth = Dimensions.get('screen').width;

const CardComponent = ({item}) => {
return (
<Animated.View style={styles.movieItem}>
<FastImage
style={styles.poster}
source={{
uri: item.img,
priority: 'high',
}}
/>
<View style={styles.innerContainer}>
<Text style={styles.title}>{item?.title}</Text>
</View>
</Animated.View>
);
};

const ListEmptyComponent = () => {
return (
<View style={styles.center}>
<Text style={styles.demoTitle}>FlatList Empty</Text>
</View>
);
};

const AnimatedFlatListIntegration = () => {
const data = [
{
title: 'Formentera Ship',
authorName: 'Roberto H',
img: 'https://images.unsplash.com/photo-1542397284385-6010376c5337?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Sunset Ship',
authorName: 'Bruce Warrington',
img: 'https://images.unsplash.com/photo-1552353617-3bfd679b3bdd?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Green Boats',
authorName: 'Ian Badenhorst',
img: 'https://images.unsplash.com/photo-1575893240675-17e719ffa7c5?q=80&w=2912&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Moisture on car',
authorName: 'Simon Goetz',
img: 'https://images.unsplash.com/photo-1498528738175-10068e55f9a7?q=80&w=2001&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Instagram',
authorName: 'Ian Badenhorst',
img: 'https://images.unsplash.com/photo-1572573309811-48474d1891b7?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Rabbit',
authorName: 'Rabbit man',
img: 'https://images.pexels.com/photos/372166/pexels-photo-372166.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
},

{
title: 'Parrot',
authorName: 'Parrot man',
img: 'https://images.pexels.com/photos/1661179/pexels-photo-1661179.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
},
];

const [search, setSearch] = useState('');

const filteredData = data?.filter(item => {
if (!search) {
return true;
}
if (
item?.title?.toLocaleLowerCase()?.includes(search?.toLocaleLowerCase())
) {
return true;
} else {
return false;
}
});

return (
<GestureHandlerRootView>
<SafeAreaView style={styles.container}>
<View style={styles.center}>
<TextInput
style={styles.input}
value={search}
onChangeText={e => setSearch(e)}
placeholder={'Search Movies...'}
placeholderTextColor="black"
/>
</View>

<Animated.FlatList
itemLayoutAnimation={LinearTransition}
data={filteredData}
renderItem={CardComponent}
keyExtractor={item => item?.title}
showsVerticalScrollIndicator={false}
ListEmptyComponent={ListEmptyComponent}
initialNumToRender={15}
/>
</SafeAreaView>
</GestureHandlerRootView>
);
};

export default AnimatedFlatListIntegration;

const styles = StyleSheet.create({
input: {
borderColor: '#00A9F1',
borderRadius: 200,
borderWidth: 1,
width: screenWidth * 0.85,
padding: 10,
color: 'black',
fontFamily: 'Pangram-Regular',
fontSize: 16,
},

container: {
flex: 1,
backgroundColor: 'white',
},
center: {
alignItems: 'center',
justifyContent: 'center',
marginVertical: 10,
},

movieItem: {
marginVertical: 10,
alignItems: 'center',
backgroundColor: 'white',
marginHorizontal: 10,
borderColor: '#00A9F1',
borderWidth: 1,
borderRadius: 15,
},
poster: {
width: screenWidth * 0.95,
height: 240,
resizeMode: 'cover',
borderRadius: 15,
},
movieDetails: {
padding: 5,
paddingHorizontal: 7,
alignItems: 'flex-start',
width: screenWidth * 0.4,
width: screenWidth * 0.9,
},
title: {
fontSize: 20,
marginBottom: 5,
fontFamily: 'Pangram-Bold',
lineHeight: 26,
color: 'black',
},
demoTitle: {
fontSize: 22,
marginBottom: 5,
fontFamily: 'Pangram-Bold',
lineHeight: 26,
marginVertical: 10,
color: 'black',
},
description: {
fontSize: 17,
fontFamily: 'Pangram-Regular',
lineHeight: 26,
marginBottom: 10,
textAlign: 'left',
color: 'black',
},
about: {
fontSize: 17,
fontFamily: 'Pangram-Regular',
lineHeight: 30,
marginVertical: 10,
marginHorizontal: 20,

color: 'black',
},
searchDescription: {
fontSize: 22,
fontFamily: 'Pangram-Regular',
marginBottom: 10,
},
innerContainer: {
padding: 20,
},
searchLoader: {
paddingTop: 30,
marginTop: 20,
marginBottom: 30,
},
searchMovies: {
width: screenWidth * 0.8,
height: 300,
},
});

Here is a neat working video of the above code:

Multicolumn setup with numColumns prop

To implement multi column list in your React Native app, you can use a prop called numColumns.

With help of this we can have 2, 3 and so on.. number of columns per row.

Here is the code to this.

import {
Dimensions,
FlatList,
SafeAreaView,
StyleSheet,
Text,
View,
Image,
} from 'react-native';
import React, {useState} from 'react';

const screenWidth = Dimensions.get('screen').width;

const CardComponent = ({item}) => {
return (
<View style={styles.movieItem}>
<Image
style={styles.poster}
source={{
uri: item.img,
}}
/>

</View>
);
};

const ListHeaderComponent = () => {
return (
<View style={styles.center}>
<Text style={styles.demoTitle}>FlatList Demo</Text>
</View>
);
};

const ListFooterComponent = () => {
return (
<View style={styles.center}>
<Text style={styles.demoTitle}>FlatList Footer</Text>
</View>
);
};

const ListEmptyComponent = () => {
return (
<View style={styles.center}>
<Text style={styles.demoTitle}>FlatList Empty</Text>
</View>
);
};

const FlatListIntegration = () => {
const [isRefreshing, setIsRefreshing] = useState(false);

const data = [
{
title: 'Formentera Ship',
authorName: 'Roberto H',
img: 'https://images.unsplash.com/photo-1542397284385-6010376c5337?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Sunset Ship',
authorName: 'Bruce Warrington',
img: 'https://images.unsplash.com/photo-1552353617-3bfd679b3bdd?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Green Boats',
authorName: 'Ian Badenhorst',
img: 'https://images.unsplash.com/photo-1575893240675-17e719ffa7c5?q=80&w=2912&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Moisture on car',
authorName: 'Simon Goetz',
img: 'https://images.unsplash.com/photo-1498528738175-10068e55f9a7?q=80&w=2001&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},
];

return (
<SafeAreaView style={styles.container}>
<FlatList
data={data}
renderItem={CardComponent}
keyExtractor={item => item?.title}
showsVerticalScrollIndicator={false}
numColumns={3}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={ListEmptyComponent}
/>
</SafeAreaView>
);
};

export default FlatListIntegration;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
center: {
alignItems: 'center',
justifyContent: 'center',
},

movieItem: {
marginVertical: 10,
alignItems: 'center',
backgroundColor: 'white',
marginHorizontal: 5,
borderColor: '#00A9F1',
borderWidth: 1,
borderRadius: 15,
},
poster: {
width: screenWidth * 0.3,
height: 120,
resizeMode: 'cover',
borderRadius: 15,
},
movieDetails: {
padding: 5,
paddingHorizontal: 7,
alignItems: 'flex-start',
width: screenWidth * 0.3,
},
title: {
fontSize: 20,
marginBottom: 5,
fontFamily: 'Pangram-Bold',
lineHeight: 26,
color: 'black',
},
demoTitle: {
fontSize: 22,
marginBottom: 5,
fontFamily: 'Pangram-Bold',
lineHeight: 26,
marginVertical: 10,
color: 'black',
},
description: {
fontSize: 17,
fontFamily: 'Pangram-Regular',
lineHeight: 26,
marginBottom: 10,
textAlign: 'left',
color: 'black',
},
about: {
fontSize: 17,
fontFamily: 'Pangram-Regular',
lineHeight: 30,
marginVertical: 10,
marginHorizontal: 20,

color: 'black',
},
searchDescription: {
fontSize: 22,
fontFamily: 'Pangram-Regular',
marginBottom: 10,
},
searchLoader: {
paddingTop: 30,
marginTop: 20,
marginBottom: 30,
},
searchMovies: {
width: screenWidth * 0.8,
height: 300,
},
});

Bonus: Handling Failed scrollToIndex Condition

What happens when the required amount of elements are not present in the data for the FlatList ScrollToIndex to work? The app will error out explaining “out of index” error.

To prevent this we can use a neat prop provided by FlatList called onScrollToIndexFailed to handle this error.

In the multiple apps I have developed the main reason for this issue a-raising is,

  1. The data fetching would have failed due to a network issue.
  2. If the given array is dynamic and contains lesser elements or no elements as expected. (Because errors happen)

So here is the code to solve this issue. You can use setTimeout to retry the scrollToIndex to work once data is there etc.

     <FlatList
ref={flatListRef}
data={data}
showsHorizontalScrollIndicator={false}
renderItem={AvatarComponent}
keyExtractor={item => item?.img}
horizontal={true}
contentContainerStyle={styles.container}
onScrollToIndexFailed={() => {
// Optional
setTimeout(() => {
flatListRef.current.scrollToIndex({index: 0, animated: true});
}, 500);
}}
/>

Instagram Styled Status Circles

Now comes the most exciting part and the conclusion to the 2 part FlatList Series: Instagram Styled Status Circles Implementation with FlatList.

We will be utilising all the concepts we have discussed in this post as well as the previous post and make a neat Instagram styled Status Circles with the green and purple status ring around the user avatar circle as well as implement scroll to index function to move to a particular user circle when its tapped on.

This can be neatly done in FlatList via the below code. I have also attached the working video of it as well.


import {
FlatList,
Pressable,
SafeAreaView,
StyleSheet,
View,
} from 'react-native';
import React, {useRef} from 'react';
import FastImage from 'react-native-fast-image';

const InstagramStatusFlatListIntegration = () => {
const data = [
{
title: 'Formentera Ship',
authorName: 'Roberto H',
img: 'https://images.unsplash.com/photo-1542397284385-6010376c5337?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Sunset Ship',
authorName: 'Bruce Warrington',
img: 'https://images.unsplash.com/photo-1552353617-3bfd679b3bdd?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Green Boats',
authorName: 'Ian Badenhorst',
img: 'https://images.unsplash.com/photo-1575893240675-17e719ffa7c5?q=80&w=2912&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Moisture on car',
authorName: 'Simon Goetz',
img: 'https://images.unsplash.com/photo-1498528738175-10068e55f9a7?q=80&w=2001&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Instagram',
authorName: 'Ian Badenhorst',
img: 'https://images.unsplash.com/photo-1572573309811-48474d1891b7?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
},

{
title: 'Rabbit',
authorName: 'Rabbit man',
img: 'https://images.pexels.com/photos/372166/pexels-photo-372166.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
},

{
title: 'Parrot',
authorName: 'Parrot man',
img: 'https://images.pexels.com/photos/1661179/pexels-photo-1661179.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
},
];

const flatListRef = useRef();

function moveForward(position) {
flatListRef.current.scrollToIndex({index: position, animated: true});
}

const AvatarComponent = ({item, index}) => {
function move() {
moveForward(index);
}
return (
<Pressable
onPress={move}
style={{
...styles.statusCircle,
borderColor:
index <= 3 ? (index === 1 ? '#6DD53C' : '#ED008F') : '#98A5BB',
}}>
<View>
<FastImage
style={styles.avatar}
source={{
uri: item.img,
priority: 'high',
}}
/>
</View>
</Pressable>
);
};

return (
<SafeAreaView style={styles.container}>
<FlatList
ref={flatListRef}
data={data}
showsHorizontalScrollIndicator={false}
renderItem={AvatarComponent}
keyExtractor={item => item?.img}
horizontal={true}
contentContainerStyle={styles.container}
onScrollToIndexFailed={() => {
// Optional
setTimeout(() => {
flatListRef.current.scrollToIndex({index: 0, animated: true});
}, 500);
}}
/>
</SafeAreaView>
);
};

export default InstagramStatusFlatListIntegration;

const styles = StyleSheet.create({
container: {
marginVertical: 10,
},
statusCircle: {
marginHorizontal: 5,
borderRadius: 100,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2,
width: 80,
height: 80,
},

avatar: {
width: 70,
height: 70,
resizeMode: 'cover',
borderRadius: 100,
},
});

Here is the demo:

Instagram Style Status Bar Integration in React Native with FlatList

We have seen a comprehensive guide on how you can integrate FlatList in React Native and leverage its capabilities to make a robust mobile application for your business needs.

Follow me on X (Twitter) for more concepts like this.

Gautham Vijayan

If you need help integrating these features or work with/for you in React Native projects or teach/tutor you in React Native reach out to me as well.

Will meet you in the next post.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Gautham Vijayan
Gautham Vijayan

Written by Gautham Vijayan

Lead Frontend React & React Native mobile app developer

No responses yet

Write a response