Understanding FlatList in React Native
The FlatList component is frequently used in React Native to display data in a scrollable list view. It’s versatile and can be applied in various scenarios, such as fetching data from an API and displaying it in a FlatList, creating a dropdown menu, or constructing a non-scrollable list with similar items. Now, let’s explore this further by working on the Counter Project, where we will create multiple counters and display them using a FlatList.
Let’s move forward with the Counter Project and enhance it by adding a FlatList that includes multiple counters. To begin, clone the project with the following command:
git clone https://github.com/devnur-org/rn-expo-counter
The project has multiple checkpoints set up as branches, so let’s switch to the specific checkpoint by using the following command.
git checkout learning-ui
Install the dependencies
yarn
Run the Metro Server and run the application on any platform
yarn start --reset-cache
Looking to learn about user interfaces in React Native or start building the counter project from scratch? You can choose your starting point here.
Once you run the application, you may see it in its current state as displayed below.
As you can observe, there are currently two counters, and the component does not have any list behavior. For demonstration purposes, we could try adding around 10 more similar counters to see how much code repetition is required.
import {SafeAreaView, StatusBar, StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<View style={styles.countersWrapper}>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
</View>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
countersWrapper: {
padding: 16,
rowGap: 16
}
});
There are many issues with this type of approach of which some include:
- We are using a reusable component that is being copied and pasted over several times making data inconsistency and hard to manage
- The screen is not scrollable
- What if there were 100 counters? Will this still be a better approach?
The Better Approach! FlatLists 🚀
The ideal solution is to use a FlatList, but how do we handle data for 10 counters? First, let’s understand the type of data FlatList requires. FlatList expects an array, which can contain any type of data. It allows us to manage and display the data in the list in a flexible manner. So, let’s create a dataset for the counters in App.tsx.
import {SafeAreaView, StatusBar, StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';
const counters = [
{
id: "0",
header: "Daily Water",
description: "Counting my water intake per day",
},
{
id: "1",
header: "Daily Water",
description: "Counting my water intake per day",
}
]
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<View style={styles.countersWrapper}>
<Counter
header={"Daily Water"}
description={"Counting my water intake per day"}
/>
<Counter
header={"Gaming Hours"}
description={"Counting my gaming hours per week"}
/>
</View>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
countersWrapper: {
padding: 16,
rowGap: 16
}
});
Delete everything from lines 22 to 31, as we will be adding the FlatList component.
import {SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
const counters = [
{
id: "0",
header: "Daily Water",
description: "Counting my water intake per day",
},
{
id: "1",
header: "Daily Water",
description: "Counting my water intake per day",
}
]
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});
After the Header, let’s add the FlatList component with some of the necessary props
import {FlatList, SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
import Counter from "./src/components/Counter";
const counters = [
{
id: "0",
header: "Daily Water",
description: "Counting my water intake per day",
},
{
id: "1",
header: "Daily Water",
description: "Counting my water intake per day",
}
]
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<FlatList
data={counters}
renderItem={({item}) => {
return <Counter header={item.header} description={item.description}/>
}}
/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});
Let’s explore the essential props we used with the FlatList component:
data
: This is a required prop for FlatList and is an array containing the data that should be displayed as a list.renderItem
: This is another required prop for FlatList that enables customization of each list item by providing a component. The renderItem prop expects a callback function that renders each item within the FlatList, allowing you to define a consistent design for each data item.
The remaining task is to improve the UI to prevent the counters from touching the edges. Initially, we used a view to contain the counters, but now we can utilize the contentContainerStyle
prop to add padding and spacing between rows, achieving the desired user interface. And let’s update the data for the second object so we don’t have similar counters.
import {FlatList, SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
import Counter from "./src/components/Counter";
const counters = [
{
id: "0",
header: "Water intake",
description: "Counting my water intake per day",
},
{
id: "1",
header: "Gaming",
description: "Counting my gaming hours",
}
]
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<FlatList
data={counters}
contentContainerStyle={styles.counterContentContainer}
renderItem={({item}) => {
return <Counter header={item.header} description={item.description}/>
}}
/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
counterContentContainer: {
padding: 16,
rowGap: 16
}
});
Let’s add additional values to the counters array to include the following:
max
: The maximum count valueinitial
: The starting number for the countersteps
: The number of steps to increase or decrease with each increment or decrement
Once these values are added to the counters array, we can pass them from renderItem to the Counter component. Then, update the prop interface of the Counter to include these values and implement the corresponding logic in the Counter component.
import {FlatList, SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
import Counter from "./src/components/Counter";
const counters = [
{
id: "0",
header: "Water intake",
description: "Counting my water intake per day",
max: null,
initial: 0,
steps: 1
},
{
id: "1",
header: "Gaming",
description: "Counting my gaming hours",
max: 50,
initial: 0,
steps: 5
}
]
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<FlatList
data={counters}
contentContainerStyle={styles.counterContentContainer}
renderItem={({item}) => {
return (
<Counter
header={item.header}
description={item.description}
max={item.max}
steps={item.steps}
initial={item.initial}
/>
)
}}
/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
counterContentContainer: {
padding: 16,
rowGap: 16
}
});
Let’s update the Counter to implement the logic for the new values added
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";
interface CounterProps {
header: string;
description: string;
max: number | null;
initial: number;
steps: number;
}
const Counter: React.FC<CounterProps> = ({header, description, max, initial, steps}): React.JSX.Element => {
const [count, setCount] = useState(initial);
function increment() {
setCount((prevState) => prevState + steps);
}
function decrement() {
setCount((prevState) => prevState - steps);
}
function reset() {
setCount(initial);
}
const isMax = max !== null ? count === max : false
return (
<View style={styles.cardContainer}>
<View style={styles.textWrapper}>
<Text style={styles.headerText}>{header}</Text>
<Text style={styles.descriptionText}>{description}</Text>
</View>
<View style={styles.actionWrapper}>
<TouchableOpacity style={styles.actionButton} onPress={reset} disabled={count === initial}>
<Text style={styles.actionText}>Reset</Text>
</TouchableOpacity>
<View style={styles.actionWrapper}>
<CounterActionButton icon={"minus"} onPress={decrement} disabled={count === initial}/>
<Text style={styles.counterText}>{count}</Text>
<CounterActionButton icon={"plus"} onPress={increment} disabled={isMax}/>
</View>
</View>
</View>
);
}
export default Counter;
- Lines 9-10: The interface is updated to include the new values.
- Line 14: The new values are utilized by accessing them from the destructured prop object.
- Line 15: The initial value is made dynamic, based on the prop.
- Line 18: The increment function is modified to add the number of steps specified in the prop.
- Line 22: The decrement function is adjusted to subtract the number of steps specified in the prop.
- Line 26: The reset function is modified to reset the count to the initial value specified in the prop.
- Line 29: A variable named isMax is created to store a boolean indicating whether the maximum count should be used or remain false.
- Line 38: The reset button is disabled when the count matches the initial value.
- Line 42: The decrement button is disabled when the count equals the initial value, following the same logic as the reset button.
- Line 44: The increment button is disabled based on the isMax logic.
Cleaning Code
Looking at our App.tsx file, we notice the code is expanding and will continue to grow as new features are added. We can make two optimizations to manage this:
- Move the counters array into a separate JSON file.
- Update the interface in the Counter component to better integrate with FlatList, reducing the need to repeatedly specify the same props in multiple locations.
Converting data to JSON
In React Native, JSON (JavaScript Object Notation) files are automatically compiled, so we can convert the counters array into a JSON file. Begin by creating a folder named data
within the src
directory. To do this, run the following command in your terminal from the root of your project.
mkdir src/data
Now, let’s add the counters.json
file in src/data
touch src/data/counters.json
Move the counters array to the JSON file and import the JSON file in App.tsx
import {FlatList, SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
import Counter from "./src/components/Counter";
import counters from './src/data/counters.json';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<FlatList
data={counters}
contentContainerStyle={styles.counterContentContainer}
renderItem={({item}) => {
return (
<Counter
header={item.header}
description={item.description}
max={item.max}
steps={item.steps}
initial={item.initial}
/>
)
}}
/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
counterContentContainer: {
padding: 16,
rowGap: 16
}
});
The result remains unchanged, and we’ve completed our first optimization. Next, let’s optimize the FlatList‘s renderItem by moving it to its own function and applying the correct types. Finally, we’ll use the spread operator to pass the item to the Counter, eliminating the need to explicitly pass each item value as a prop.
import {FlatList, ListRenderItem, SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Header from './src/components/Header';
import Counter, {type CounterProps} from "./src/components/Counter";
import counters from './src/data/counters.json';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Header/>
<FlatList
data={counters}
contentContainerStyle={styles.counterContentContainer}
renderItem={_renderItem}
/>
<StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
</SafeAreaView>
);
}
const _renderItem: ListRenderItem<CounterProps> = ({item}) => {
return <Counter {...item} />
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
counterContentContainer: {
padding: 16,
rowGap: 16
}
});
The only change in the Counter component is to add the export
keyword to the CounterProps interface so it can be called in the App.tsx
file.
export interface CounterProps {
header: string;
description: string;
max: number | null;
initial: number;
steps: number;
}
Congratulations 🎉 on learning about FlatList! We hope you found this tutorial helpful. Feel free to continue experimenting by adding more values to the counters.json file and exploring additional possibilities.