Skip to content

List Components

List provides versatile vertical and horizontal list components built on React Native’s FlatList with enhanced features including infinite scrolling, hide-on-scroll animations, theming integration, and flexible styling options.

import {
VList,
HList,
VFlashList,
HFlashList
} from 'rnc-theme';
import type { ListProps, InfiniteScrollProps } from 'rnc-theme';
  • VList - Vertical list component based on FlatList
  • HList - Horizontal list component based on FlatList

High-Performance List Components New

Section titled “High-Performance List Components ”
  • VFlashList - Vertical high-performance list based on Shopify’s FlashList
  • HFlashList - Horizontal high-performance list based on Shopify’s FlashList
const data = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
];
<VList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ padding: 16 }}>
<Text>{item.name}</Text>
<Text>{item.email}</Text>
</View>
)}
/>
PropTypeDefaultDescription
datareadonly T[]-Array of data items to render
renderItem(item: { item: T; index: number }) => React.ReactElement-Function to render each list item
keyExtractor(item: T, index: number) => string-Function to extract unique key for each item
paddingkeyof Theme['spacing']-Internal padding using theme spacing
marginkeyof Theme['spacing']-External margin using theme spacing
backgroundColorkeyof Theme['colors']'background'Background color from theme
borderRadiuskeyof Theme['components']['borderRadius']-Border radius from theme
themedbooleanfalseEnable theme-based styling
hideOnScrollHideOnScrollConfig-Configuration for hide-on-scroll animation
infiniteScrollInfiniteScrollProps-Configuration for infinite scrolling

FlashListProps<T> New (VFlashList & HFlashList)

Section titled “FlashListProps<T> (VFlashList & HFlashList)”
PropTypeDefaultDescription
dataT[]-Array of data items to render
renderItem({ item: T, index: number }) => React.ReactElement-Function to render each item
keyExtractor(item: T) => string-Function to extract unique key
estimatedItemSizenumber50 (V) / 100 (H)Estimated size of each item in pixels
estimatedListSize{ height: number, width: number }-Estimated total list dimensions
drawDistancenumber250Distance in pixels to pre-render items
disableAutoLayoutbooleanfalseDisable automatic layout calculations
themedbooleanfalseApply theme styles automatically
infiniteScrollInfiniteScrollProps-Configuration for infinite scrolling
hideOnScrollHideOnScrollConfig-Configuration for hide-on-scroll animation
PropTypeDefaultDescription
onLoadMore() => void-Callback when more items should be loaded
loadingbooleanfalseWhether currently loading more items
hasMorebooleantrueWhether more items are available
thresholdnumber0.1Distance from end to trigger load more (0-1)
PropTypeDefaultDescription
heightnumber-Height of the element to hide/show
durationnumber300Animation duration in milliseconds
thresholdnumber10Scroll threshold to trigger animation
scrollDirectionScrollDirectionType-Direction of scroll to trigger hide
hideDirectionHideDirectionType-Direction to hide the element
result(value: HideOnScrollResult | null) => void-Callback with animation state

High-Performance Social Media Feed New

Section titled “High-Performance Social Media Feed ”
const SocialFeed = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const loadMorePosts = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const newPosts = await fetchPosts(posts.length, 10);
setPosts(prev => [...prev, ...newPosts]);
setHasMore(newPosts.length === 10);
} catch (error) {
console.error('Failed to load posts:', error);
} finally {
setLoading(false);
}
}, [posts.length, loading, hasMore]);
const renderPost = useCallback(({ item }: { item: Post }) => (
<Card style={styles.postCard}>
<Box style={styles.postHeader}>
<Image source={{ uri: item.avatar }} style={styles.avatar} />
<Box style={styles.userInfo}>
<Typography weight="semibold">{item.username}</Typography>
<Typography variant="caption" color="muted">
{item.timestamp}
</Typography>
</Box>
</Box>
<Image source={{ uri: item.image }} style={styles.postImage} />
<Box style={styles.postActions}>
<TouchableOpacity style={styles.actionButton}>
<Typography>❤️ {item.likes}</Typography>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Typography>💬 {item.comments}</Typography>
</TouchableOpacity>
</Box>
<Box style={styles.postCaption}>
<Typography>
<Typography weight="semibold">{item.username}</Typography>
{' '}{item.caption}
</Typography>
</Box>
</Card>
), []);
return (
<VFlashList
data={posts}
renderItem={renderPost}
keyExtractor={(item) => item.id}
estimatedItemSize={400}
estimatedListSize={{ height: 600, width: width }}
infiniteScroll={{
onLoadMore: loadMorePosts,
loading,
hasMore,
threshold: 0.5
}}
themed
backgroundColor="background"
showsVerticalScrollIndicator={false}
/>
);
};
const StoriesCarousel = () => {
const [stories, setStories] = useState<Story[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const loadMoreStories = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
const newStories = await fetchStories(stories.length, 10);
setStories(prev => [...prev, ...newStories]);
setHasMore(newStories.length === 10);
setLoading(false);
}, [stories.length, loading, hasMore]);
const renderStory = useCallback(({ item }: { item: Story }) => (
<TouchableOpacity style={styles.storyItem}>
<Box style={styles.storyContainer}>
<Image source={{ uri: item.avatar }} style={styles.storyAvatar} />
<Box style={styles.storyImageWrapper}>
<Image source={{ uri: item.image }} style={styles.storyImage} />
</Box>
</Box>
<Typography
style={styles.storyUsername}
numberOfLines={1}
>
{item.username}
</Typography>
<Typography
variant="caption"
style={styles.storyTime}
>
{item.timestamp}
</Typography>
</TouchableOpacity>
), []);
return (
<Box style={styles.storiesSection}>
<Typography variant="h6" style={styles.sectionTitle}>
Stories
</Typography>
<HFlashList
data={stories}
renderItem={renderStory}
keyExtractor={(item) => item.id}
estimatedItemSize={100}
estimatedListSize={{ height: 120, width: width }}
infiniteScroll={{
onLoadMore: loadMoreStories,
loading,
hasMore,
threshold: 0.8
}}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.storiesContent}
/>
</Box>
);
};
const ProductList = ({ products }) => {
const renderProduct = ({ item, index }) => (
<View style={styles.productItem}>
<Image source={{ uri: item.image }} style={styles.productImage} />
<View style={styles.productInfo}>
<Text style={styles.productName}>{item.name}</Text>
<Text style={styles.productPrice}>${item.price}</Text>
<Text style={styles.productDescription}>{item.description}</Text>
</View>
</View>
);
return (
<VList
data={products}
keyExtractor={(item) => item.id.toString()}
renderItem={renderProduct}
padding="md"
backgroundColor="background"
showsVerticalScrollIndicator={false}
/>
);
};
const styles = StyleSheet.create({
productItem: {
flexDirection: 'row',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
productImage: {
width: 80,
height: 80,
borderRadius: 8,
marginRight: 12,
},
productInfo: {
flex: 1,
justifyContent: 'center',
},
productName: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
productPrice: {
fontSize: 18,
color: '#007AFF',
fontWeight: '600',
marginBottom: 4,
},
productDescription: {
fontSize: 14,
color: '#666',
},
});

Advanced FlashList with Custom Hook New

Section titled “Advanced FlashList with Custom Hook ”
const useInfiniteList = <T>(
fetchFunction: (offset: number, limit: number) => Promise<T[]>,
limit: number = 20
) => {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
setError(null);
try {
const newData = await fetchFunction(data.length, limit);
setData(prev => [...prev, ...newData]);
setHasMore(newData.length === limit);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load data');
} finally {
setLoading(false);
}
}, [data.length, loading, hasMore, fetchFunction, limit]);
const refresh = useCallback(async () => {
setData([]);
setHasMore(true);
setError(null);
try {
const newData = await fetchFunction(0, limit);
setData(newData);
setHasMore(newData.length === limit);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to refresh data');
}
}, [fetchFunction, limit]);
const reset = useCallback(() => {
setData([]);
setLoading(false);
setHasMore(true);
setError(null);
}, []);
return {
data,
loading,
hasMore,
error,
loadMore,
refresh,
reset
};
};
// Usage
const MyList = () => {
const { data, loading, hasMore, loadMore, refresh } = useInfiniteList(
fetchItems,
15
);
return (
<VFlashList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
estimatedItemSize={200}
infiniteScroll={{ onLoadMore: loadMore, loading, hasMore }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={refresh} />
}
/>
);
};

Infinite Scroll Implementation (Standard FlatList)

Section titled “Infinite Scroll Implementation (Standard FlatList)”
const InfiniteProductList = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const loadMoreProducts = async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await fetchProducts(page);
if (response.data.length === 0) {
setHasMore(false);
} else {
setProducts(prev => [...prev, ...response.data]);
setPage(prev => prev + 1);
}
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMoreProducts();
}, []);
const renderProduct = ({ item }) => (
<ProductCard product={item} />
);
const renderFooter = () => {
if (!loading) return null;
return (
<View style={styles.loadingFooter}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading more products...</Text>
</View>
);
};
return (
<VList
data={products}
keyExtractor={(item) => item.id.toString()}
renderItem={renderProduct}
ListFooterComponent={renderFooter}
infiniteScroll={{
onLoadMore: loadMoreProducts,
loading: loading,
hasMore: hasMore,
threshold: 0.2,
}}
padding="md"
backgroundColor="background"
/>
);
};
FeatureFlatListFlashListPerformance Impact
Memory UsageHighLow📈 90% reduction
Scroll PerformanceGoodExcellent📈 60% improvement
Large Lists (1000+ items)SlowFast📈 5x faster
Initial RenderFastVery Fast📈 40% faster
Layout CalculationsAutomaticOptimized📈 Performance boost
// Vertical scrolling list - Standard FlatList
<VList
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
padding="md"
/>
<FlatList
data={items}
renderItem={({ item }) => <ItemComponent item={item} />}
keyExtractor={(item) => item.id}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
refreshing={refreshing}
onRefresh={onRefresh}
/>
<VList
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
infiniteScroll={{
onLoadMore: loadMore,
loading: loading,
hasMore: hasMore
}}
/>

When to Use FlashList

New
  • Lists with 1000+ items for maximum performance benefit
  • High-frequency scrolling scenarios (social feeds, chat messages)
  • Memory-constrained environments
  • Complex list items with heavy rendering
  • Real-time data updates requiring smooth performance

When to Use Standard List

  • Simple lists with fewer than 100 items
  • Static content that doesn’t change frequently
  • Prototype/development phase before optimization
  • Legacy code that doesn’t require performance improvements
  • Simple use cases where FlashList overhead isn’t justified

Performance Optimization

  • Use estimatedItemSize as close to actual item size as possible
  • Implement keyExtractor that returns stable, unique keys
  • Memoize renderItem functions using useCallback
  • Use React.memo for item components that don’t need frequent re-renders
  • Set appropriate drawDistance based on your use case

Memory Management

  • Use removeClippedSubviews for very long lists
  • Implement proper data cleanup in infinite scroll scenarios
  • Avoid storing large objects in list item state
  • Consider implementing data pagination for very large datasets

User Experience

  • Implement pull-to-refresh for data that can be updated
  • Use loading indicators during infinite scroll
  • Provide empty states with helpful messaging
  • Consider skeleton screens for initial loading states
  • Implement proper error handling and retry mechanisms
const CustomListScreen = () => {
const listRef = useRef<FlatList>(null);
const scrollToTop = () => {
listRef.current?.scrollToOffset({ offset: 0, animated: true });
};
const scrollToItem = (index: number) => {
listRef.current?.scrollToIndex({ index, animated: true });
};
return (
<View style={styles.container}>
<VList
ref={listRef}
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
padding="lg"
getItemLayout={(data, index) => ({
length: 100,
offset: 100 * index,
index,
})}
/>
<TouchableOpacity style={styles.scrollButton} onPress={scrollToTop}>
<Text>Scroll to Top</Text>
</TouchableOpacity>
</View>
);
};
const OptimizedFlashList = React.memo(({ data }) => {
const renderItem = useCallback(({ item, index }) => (
<MemoizedListItem item={item} index={index} />
), []);
const keyExtractor = useCallback((item) => item.id.toString(), []);
return (
<VFlashList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
estimatedItemSize={ITEM_HEIGHT}
drawDistance={500} // Increase for smoother scrolling
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
removeClippedSubviews={true}
backgroundColor="background"
/>
);
});
const MemoizedListItem = React.memo(({ item, index }) => (
<View style={[styles.item, { height: ITEM_HEIGHT }]}>
<Text>{item.title}</Text>
</View>
));
const SearchableList = () => {
const [data, setData] = useState(originalData);
const [searchQuery, setSearchQuery] = useState('');
const [selectedFilter, setSelectedFilter] = useState('all');
const filteredData = useMemo(() => {
let filtered = data;
// Apply search filter
if (searchQuery) {
filtered = filtered.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Apply category filter
if (selectedFilter !== 'all') {
filtered = filtered.filter(item => item.category === selectedFilter);
}
return filtered;
}, [data, searchQuery, selectedFilter]);
return (
<View style={styles.container}>
<TextInput
style={styles.searchInput}
placeholder="Search items..."
value={searchQuery}
onChangeText={setSearchQuery}
/>
<HList
data={filterOptions}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<FilterButton
filter={item}
selected={selectedFilter === item.id}
onPress={() => setSelectedFilter(item.id)}
/>
)}
style={styles.filterList}
/>
<VFlashList
data={filteredData}
keyExtractor={(item) => item.id}
renderItem={renderItem}
estimatedItemSize={120}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text>No items found</Text>
</View>
}
padding="md"
/>
</View>
);
};