DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 12: Lists and Data Display (FlatList, SectionList)

Lists and Data Display

Introduction

In mobile app development, displaying lists of data is one of the most common requirements. Whether it’s a social media feed, a shopping cart, or a contact list, efficient list rendering is crucial for app performance and user experience. React Native provides powerful components like FlatList and SectionList that are optimized for rendering large datasets with smooth scrolling performance. This chapter will explore Lists and Data Display and how to effectively use these components to create dynamic, performant lists in your React Native applications.

Why Not Use ScrollView for Lists?

Before diving into FlatList and SectionList, it’s important to understand why we don’t use ScrollView for large lists:

Problems with ScrollView for Large Lists:

  • Memory Issues: Renders all items at once, consuming excessive memory
  • Performance Degradation: Slow initial load times for large datasets
  • Poor User Experience: Laggy scrolling with many items
  • Battery Drain: Continuous rendering impacts device battery life

Solution: Virtualized Lists

React Native’s FlatList and SectionList use virtualization, which means they only render items currently visible on screen plus a small buffer, dramatically improving performance.

FlatList Fundamentals

FlatList is a performant interface for rendering basic, flat lists with the following features:

  • Cross-platform compatibility
  • Scroll loading (lazy loading)
  • Pull-to-refresh functionality
  • Header and footer support
  • Horizontal and vertical scrolling

Basic FlatList Implementation

import React from 'react';
import { FlatList, Text, View, StyleSheet } from 'react-native';

const BasicFlatListExample = () => {
  const data = [
    { id: '1', title: 'First Item' },
    { id: '2', title: 'Second Item' },
    { id: '3', title: 'Third Item' },
    { id: '4', title: 'Fourth Item' },
    { id: '5', title: 'Fifth Item' },
  ];

  const renderItem = ({ item }) => (
    <View style={styles.item}>
      <Text style={styles.title}>{item.title}</Text>
    </View>
  );

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default BasicFlatListExample;

Essential FlatList Props

Required Props:

  • data: Array of data to render
  • renderItem: Function that renders each item
  • keyExtractor: Function that extracts unique keys for items

Common Optional Props:

  • ItemSeparatorComponent: Component rendered between items
  • ListHeaderComponent: Component rendered at the top
  • ListFooterComponent: Component rendered at the bottom
  • ListEmptyComponent: Component rendered when data is empty
  • horizontal: Boolean for horizontal scrolling
  • numColumns: Number of columns for grid layout

Advanced FlatList Example

import React, { useState } from 'react';
import {
  FlatList,
  Text,
  View,
  StyleSheet,
  TouchableOpacity,
  RefreshControl,
  ActivityIndicator,
} from 'react-native';

const AdvancedFlatListExample = () => {
  const [data, setData] = useState([
    { id: '1', title: 'Item 1', description: 'Description for item 1' },
    { id: '2', title: 'Item 2', description: 'Description for item 2' },
    { id: '3', title: 'Item 3', description: 'Description for item 3' },
    { id: '4', title: 'Item 4', description: 'Description for item 4' },
    { id: '5', title: 'Item 5', description: 'Description for item 5' },
  ]);
 
  const [refreshing, setRefreshing] = useState(false);
  const [loading, setLoading] = useState(false);

  const onRefresh = () => {
    setRefreshing(true);
    // Simulate API call
    setTimeout(() => {
      setData(prev => [...prev, {
        id: Date.now().toString(),
        title: 'New Item',
        description: 'Fresh content'
      }]);
      setRefreshing(false);
    }, 2000);
  };

  const loadMore = () => {
    if (loading) return;
    setLoading(true);
    // Simulate loading more data
    setTimeout(() => {
      const newData = Array.from({ length: 5 }, (_, i) => ({
        id: (Date.now() + i).toString(),
        title: `Item ${data.length + i + 1}`,
        description: `Description for item ${data.length + i + 1}`,
      }));
      setData(prev => [...prev, ...newData]);
      setLoading(false);
    }, 1500);
  };

  const renderItem = ({ item }) => (
    <TouchableOpacity style={styles.item}>
      <Text style={styles.title}>{item.title}</Text>
      <Text style={styles.description}>{item.description}</Text>
    </TouchableOpacity>
  );

  const renderSeparator = () => <View style={styles.separator} />;

  const renderHeader = () => (
    <View style={styles.header}>
      <Text style={styles.headerText}>My Awesome List</Text>
    </View>
  );

  const renderFooter = () => (
    <View style={styles.footer}>
      {loading && <ActivityIndicator size="large" color="#0000ff" />}
    </View>
  );

  const renderEmpty = () => (
    <View style={styles.emptyContainer}>
      <Text style={styles.emptyText}>No items found</Text>
    </View>
  );

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      ItemSeparatorComponent={renderSeparator}
      ListHeaderComponent={renderHeader}
      ListFooterComponent={renderFooter}
      ListEmptyComponent={renderEmpty}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      showsVerticalScrollIndicator={false}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#ffffff',
    padding: 20,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  description: {
    fontSize: 14,
    color: '#666',
  },
  separator: {
    height: 1,
    backgroundColor: '#e0e0e0',
    marginHorizontal: 16,
  },
  header: {
    padding: 20,
    backgroundColor: '#f0f0f0',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 24,
    fontWeight: 'bold',
  },
  footer: {
    padding: 20,
    alignItems: 'center',
  },
  emptyContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 40,
  },
  emptyText: {
    fontSize: 16,
    color: '#888',
  },
});

export default AdvancedFlatListExample;

Horizontal FlatList

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

const HorizontalFlatListExample = () => {
  const { width } = Dimensions.get('window');
 
  const data = Array.from({ length: 10 }, (_, i) => ({
    id: i.toString(),
    title: `Card ${i + 1}`,
    color: `hsl(${i * 36}, 70%, 50%)`,
  }));

  const renderItem = ({ item }) => (
    <View style={[styles.card, { backgroundColor: item.color }]}>
      <Text style={styles.cardTitle}>{item.title}</Text>
    </View>
  );

  return (
    <View style={styles.container}>
      <Text style={styles.sectionTitle}>Horizontal Scrolling Cards</Text>
      <FlatList
        data={data}
        renderItem={renderItem}
        keyExtractor={item => item.id}
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        contentContainerStyle={styles.horizontalList}
        snapToInterval={width * 0.8 + 20}
        decelerationRate="fast"
        snapToAlignment="center"
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 50,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  horizontalList: {
    paddingHorizontal: 10,
  },
  card: {
    width: Dimensions.get('window').width * 0.8,
    height: 200,
    marginRight: 20,
    borderRadius: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  cardTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    color: 'white',
  },
});

export default HorizontalFlatListExample;

Grid Layout with FlatList

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

const GridFlatListExample = () => {
  const { width } = Dimensions.get('window');
  const numColumns = 2;
  const itemSize = (width - 48) / numColumns; // 48 = padding + margins

  const data = Array.from({ length: 20 }, (_, i) => ({
    id: i.toString(),
    title: `Item ${i + 1}`,
    color: `hsl(${i * 18}, 70%, 50%)`,
  }));

  const renderItem = ({ item }) => (
    <View style={[styles.gridItem, {
      width: itemSize,
      height: itemSize,
      backgroundColor: item.color
    }]}>
      <Text style={styles.gridItemText}>{item.title}</Text>
    </View>
  );

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      numColumns={numColumns}
      contentContainerStyle={styles.gridContainer}
      columnWrapperStyle={styles.row}
    />
  );
};

const styles = StyleSheet.create({
  gridContainer: {
    padding: 16,
  },
  row: {
    justifyContent: 'space-between',
  },
  gridItem: {
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: 16,
    borderRadius: 8,
  },
  gridItemText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 16,
  },
});

export default GridFlatListExample;

SectionList Fundamentals

SectionList is perfect for displaying data organized into logical sections, like contacts grouped by alphabet or items categorized by type.

Basic SectionList Implementation

import React from 'react';
import { SectionList, Text, View, StyleSheet } from 'react-native';

const BasicSectionListExample = () => {
  const DATA = [
    {
      title: 'Main dishes',
      data: ['Pizza', 'Burger', 'Risotto'],
    },
    {
      title: 'Sides',
      data: ['French Fries', 'Onion Rings', 'Fried Shrimps'],
    },
    {
      title: 'Drinks',
      data: ['Water', 'Coke', 'Beer'],
    },
    {
      title: 'Desserts',
      data: ['Cheese Cake', 'Ice Cream'],
    },
  ];

  const renderItem = ({ item }) => (
    <View style={styles.item}>
      <Text style={styles.title}>{item}</Text>
    </View>
  );

  const renderSectionHeader = ({ section: { title } }) => (
    <View style={styles.sectionHeader}>
      <Text style={styles.sectionHeaderText}>{title}</Text>
    </View>
  );

  return (
    <SectionList
      sections={DATA}
      keyExtractor={(item, index) => item + index}
      renderItem={renderItem}
      renderSectionHeader={renderSectionHeader}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9f9f9',
    padding: 20,
    marginVertical: 2,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
  sectionHeader: {
    backgroundColor: '#e0e0e0',
    padding: 12,
    marginHorizontal: 16,
  },
  sectionHeaderText: {
    fontSize: 18,
    fontWeight: 'bold',
  },
});

export default BasicSectionListExample;

Advanced SectionList Example

import React, { useState } from 'react';
import {
  SectionList,
  Text,
  View,
  StyleSheet,
  TouchableOpacity,
  Alert,
} from 'react-native';

const AdvancedSectionListExample = () => {
  const [sections, setSections] = useState([
    {
      title: 'Fruits',
      data: [
        { id: '1', name: 'Apple', price: '$2.50' },
        { id: '2', name: 'Banana', price: '$1.20' },
        { id: '3', name: 'Orange', price: '$3.00' },
      ],
    },
    {
      title: 'Vegetables',
      data: [
        { id: '4', name: 'Carrot', price: '$1.80' },
        { id: '5', name: 'Broccoli', price: '$2.20' },
        { id: '6', name: 'Spinach', price: '$1.50' },
      ],
    },
    {
      title: 'Dairy',
      data: [
        { id: '7', name: 'Milk', price: '$3.50' },
        { id: '8', name: 'Cheese', price: '$5.00' },
      ],
    },
  ]);

  const handleItemPress = (item) => {
    Alert.alert('Item Selected', `You selected ${item.name} - ${item.price}`);
  };

  const renderItem = ({ item, section }) => (
    <TouchableOpacity
      style={styles.item}
      onPress={() => handleItemPress(item)}
    >
      <View style={styles.itemContent}>
        <Text style={styles.itemName}>{item.name}</Text>
        <Text style={styles.itemPrice}>{item.price}</Text>
      </View>
    </TouchableOpacity>
  );

  const renderSectionHeader = ({ section: { title, data } }) => (
    <View style={styles.sectionHeader}>
      <Text style={styles.sectionHeaderText}>{title}</Text>
      <Text style={styles.sectionCount}>({data.length} items)</Text>
    </View>
  );

  const renderSectionFooter = ({ section }) => (
    <View style={styles.sectionFooter}>
      <Text style={styles.sectionFooterText}>
        End of {section.title} section
      </Text>
    </View>
  );

  return (
    <SectionList
      sections={sections}
      keyExtractor={(item, index) => item.id}
      renderItem={renderItem}
      renderSectionHeader={renderSectionHeader}
      renderSectionFooter={renderSectionFooter}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
      SectionSeparatorComponent={() => <View style={styles.sectionSeparator} />}
      stickySectionHeadersEnabled={true}
      showsVerticalScrollIndicator={false}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#ffffff',
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  itemContent: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  itemName: {
    fontSize: 16,
    color: '#333',
  },
  itemPrice: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#2e7d32',
  },
  sectionHeader: {
    backgroundColor: '#1976d2',
    paddingHorizontal: 16,
    paddingVertical: 12,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  sectionHeaderText: {
    fontSize: 18,
    fontWeight: 'bold',
    color: 'white',
  },
  sectionCount: {
    fontSize: 14,
    color: '#bbdefb',
  },
  sectionFooter: {
    backgroundColor: '#f5f5f5',
    paddingHorizontal: 16,
    paddingVertical: 8,
    alignItems: 'center',
  },
  sectionFooterText: {
    fontSize: 12,
    color: '#666',
    fontStyle: 'italic',
  },
  separator: {
    height: 1,
    backgroundColor: '#e0e0e0',
    marginHorizontal: 16,
  },
  sectionSeparator: {
    height: 8,
    backgroundColor: '#f5f5f5',
  },
});

export default AdvancedSectionListExample;

Performance Optimization Techniques

1. Optimize renderItem Function

import React, { memo } from 'react';
import { Text, View, StyleSheet } from 'react-native';

// Memoize the item component to prevent unnecessary re-renders
const ListItem = memo(({ item, onPress }) => (
  <TouchableOpacity style={styles.item} onPress={() => onPress(item)}>
    <Text style={styles.title}>{item.title}</Text>
  </TouchableOpacity>
));

const OptimizedFlatList = () => {
  const [data, setData] = useState([]);

  // Use useCallback to prevent renderItem from being recreated on every render
  const renderItem = useCallback(({ item }) => (
    <ListItem item={item} onPress={handleItemPress} />
  ), []);

  const handleItemPress = useCallback((item) => {
    // Handle item press
  }, []);

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      getItemLayout={(data, index) => (
        { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
      )}
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={10}
      initialNumToRender={10}
      updateCellsBatchingPeriod={50}
    />
  );
};

2. Use getItemLayout for Fixed Height Items

const ITEM_HEIGHT = 80;

const getItemLayout = (data, index) => ({
  length: ITEM_HEIGHT,
  offset: ITEM_HEIGHT * index,
  index,
});

// Apply to FlatList
<FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={item => item.id}
  getItemLayout={getItemLayout}
/>

3. Implement Proper Key Extraction

// Good - use unique, stable keys
const keyExtractor = (item, index) => item.id.toString();

// Bad - using index as key can cause issues
const keyExtractor = (item, index) => index.toString();

Common Patterns and Use Cases

1. Search with FlatList

import React, { useState, useMemo } from 'react';
import { FlatList, TextInput, View, StyleSheet } from 'react-native';

const SearchableList = () => {
  const [searchQuery, setSearchQuery] = useState('');
  const [originalData] = useState([
    { id: '1', name: 'John Doe', email: 'john@example.com' },
    { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
    { id: '3', name: 'Bob Johnson', email: 'bob@example.com' },
  ]);

  const filteredData = useMemo(() => {
    if (!searchQuery) return originalData;
    return originalData.filter(item =>
      item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
      item.email.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }, [searchQuery, originalData]);

  const renderItem = ({ item }) => (
    <View style={styles.item}>
      <Text style={styles.name}>{item.name}</Text>
      <Text style={styles.email}>{item.email}</Text>
    </View>
  );

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.searchInput}
        placeholder="Search..."
        value={searchQuery}
        onChangeText={setSearchQuery}
      />
      <FlatList
        data={filteredData}
        renderItem={renderItem}
        keyExtractor={item => item.id}
      />
    </View>
  );
};

2. Swipe to Delete

import React, { useState } from 'react';
import {
  FlatList,
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import { SwipeRow } from 'react-native-swipe-list-view';

const SwipeToDeleteList = () => {
  const [data, setData] = useState([
    { id: '1', title: 'Item 1' },
    { id: '2', title: 'Item 2' },
    { id: '3', title: 'Item 3' },
  ]);

  const deleteItem = (id) => {
    Alert.alert(
      'Delete Item',
      'Are you sure you want to delete this item?',
      [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Delete',
          style: 'destructive',
          onPress: () => {
            setData(data.filter(item => item.id !== id));
          },
        },
      ]
    );
  };

  const renderItem = ({ item }) => (
    <SwipeRow rightOpenValue={-75}>
      <View style={styles.hiddenItem}>
        <TouchableOpacity
          style={styles.deleteButton}
          onPress={() => deleteItem(item.id)}
        >
          <Text style={styles.deleteButtonText}>Delete</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.item}>
        <Text style={styles.title}>{item.title}</Text>
      </View>
    </SwipeRow>
  );

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
    />
  );
};

Best Practices

1. Memory Management

  • Use removeClippedSubviews={true} for very long lists
  • Implement getItemLayout for fixed-height items
  • Use initialNumToRender to control initial batch size
  • Set appropriate windowSize for your use case

2. Performance Optimization

  • Memoize renderItem function with useCallback
  • Use React.memo for list item components
  • Avoid complex computations in renderItem
  • Use keyExtractor with stable, unique keys

3. User Experience

  • Implement pull-to-refresh functionality
  • Add loading states for better UX
  • Handle empty states gracefully
  • Use appropriate separators and spacing

4. Data Structure

  • Keep data flat and normalized
  • Use consistent data structure across items
  • Implement proper error handling for data fetching
  • Consider pagination for large datasets

Common Pitfalls to Avoid

  1. Using array index as key: This can cause rendering issues when items are reordered
  2. Heavy computations in renderItem: Move complex logic outside the render function
  3. Not implementing pull-to-refresh: Users expect this functionality in mobile apps
  4. Ignoring empty states: Always provide feedback when lists are empty
  5. Not handling loading states: Show loading indicators during data fetching
  6. Overusing ScrollView: Use FlatList/SectionList for any list with more than a few items

Summary

This chapter covered the essential concepts of displaying lists in React Native using FlatList and SectionList. Key takeaways include:

  • FlatList is ideal for simple, flat lists with virtualization for performance
  • SectionList is perfect for grouped data with section headers
  • Performance optimization is crucial for smooth scrolling
  • Proper key extraction and memoization prevent unnecessary re-renders
  • Advanced features like pull-to-refresh and infinite scrolling enhance user experience
  • Grid layouts and horizontal scrolling provide flexible display options

In the next chapter, we’ll explore React Native Lifecycle Methods and Component Lifecycle, which will help you understand how components behave throughout their existence in your application.

Related Articles:

Scroll to Top