DEV Community

Nguyễn Hữu Hiếu
Nguyễn Hữu Hiếu

Posted on • Edited on

React Native Flatlist: Filter & Sorting

Scenarior

I read a lot of react-native flatlist guide but no guide that point enough information for this, how to use it right way, how to implement search, sort, and so on. So I decided to create one that can help you and me to ref every time working with flat list.

This guide helps you build a flat list and how to improve it based on my experiment step by step

  • Step 1: Build a flatlist
  • Step 2: Add filter condition
  • Step 3: Add highlight
  • Step 4: Expand item and stick item (only scroll content)

Step 1: Build a flatlist

 import React, {useState} from 'react'; import {FlatList, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; interface Post { id: number; title: string; description: string; } const postMocks: Post[] = [ {id: 1, title: 'Post 1', description: 'Description for Post 1'}, {id: 2, title: 'Post 2', description: 'Description for Post 2'}, {id: 3, title: 'Post 3', description: 'Description for Post 3'}, {id: 4, title: 'Post 4', description: 'Description for Post 4'}, {id: 5, title: 'Post 5', description: 'Description for Post 5'}, {id: 6, title: 'Post 6', description: 'Description for Post 6'}, {id: 7, title: 'Post 7', description: 'Description for Post 7'}, {id: 8, title: 'Post 8', description: 'Description for Post 8'}, {id: 9, title: 'Post 9', description: 'Description for Post 9'}, {id: 10, title: 'Post 10', description: 'Description for Post 10'}, {id: 11, title: 'Post 11', description: 'Description for Post 11'}, {id: 12, title: 'Post 12', description: 'Description for Post 12'}, {id: 13, title: 'Post 13', description: 'Description for Post 13'}, {id: 14, title: 'Post 14', description: 'Description for Post 14'}, {id: 15, title: 'Post 15', description: 'Description for Post 15'}, {id: 16, title: 'Post 16', description: 'Description for Post 16'}, {id: 17, title: 'Post 17', description: 'Description for Post 17'}, {id: 18, title: 'Post 18', description: 'Description for Post 18'}, {id: 19, title: 'Post 19', description: 'Description for Post 19'}, {id: 20, title: 'Post 20', description: 'Description for Post 20'}, {id: 21, title: 'Post 21', description: 'Description for Post 21'}, {id: 22, title: 'Post 22', description: 'Description for Post 22'}, {id: 23, title: 'Post 23', description: 'Description for Post 23'}, {id: 24, title: 'Post 24', description: 'Description for Post 24'}, {id: 25, title: 'Post 25', description: 'Description for Post 25'}, {id: 26, title: 'Post 26', description: 'Description for Post 26'}, {id: 27, title: 'Post 27', description: 'Description for Post 27'}, {id: 28, title: 'Post 28', description: 'Description for Post 28'}, {id: 29, title: 'Post 29', description: 'Description for Post 29'}, {id: 30, title: 'Post 30', description: 'Description for Post 30'}, ]; const PostItem = React.memo( ({item, index}: {item: Post; index: number}) => { console.log('PostItem', index); return ( <View style={postItemStyles.container}> <Text style={postItemStyles.title}>{item.title}</Text> <Text style={postItemStyles.description}>{item.description}</Text> </View> ); }, (prevProps, nextProps) => { // only re-render when item is changed return prevProps.item.id === nextProps.item.id; }, ); const postItemStyles = StyleSheet.create({ container: { backgroundColor: '#fff', padding: 10, }, title: { fontSize: 16, fontWeight: 'bold', }, description: { fontSize: 14, marginTop: 10, }, }); export const FlatListDemo = () => { const [postList, setPostList] = useState(postMocks); /** * create renderPostItem: => can reduce anonymous function in renderPostList * anonymous function will be created every time renderPostList is called => so it's better to create a function outside * @param param0 * @returns */ const renderPostItem = ({item, index}: {item: Post; index: number}) => { // alway re-render each time renderPostList re-render // to reduce re-render UI, we can use React.memo to create new component that only handle UI // check by append and remove post console.log('renderPostItem', index); return <PostItem index={index} item={item} />; }; /** * * @param item * @returns */ const keyExtractor = (item: Post) => item.id.toString(); const appendPost = () => { const newPost = { id: postList.length + 1, title: `Post ${postList.length + 1}`, description: `Description for Post ${postList.length + 1}`, }; setPostList([...postList, newPost]); }; const removeLastPost = () => { const newPostList = [...postList]; newPostList.pop(); setPostList(newPostList); }; const renderPostList = () => { return ( <FlatList style={postListStyles.container} data={postList} renderItem={renderPostItem} keyExtractor={keyExtractor} /> ); }; return ( <View style={styles.container}> <View style={styles.header}> {/* appendPost */} <TouchableOpacity onPress={appendPost} style={styles.button}> <Text>Append Post</Text> </TouchableOpacity> {/* removeLastPost */} <TouchableOpacity onPress={removeLastPost} style={styles.button}> <Text>Remove Last Post</Text> </TouchableOpacity> </View> {renderPostList()} </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', borderTopColor: '#ddd', borderTopWidth: 1, }, header: { backgroundColor: '#ddd', justifyContent: 'space-between', alignItems: 'center', flexDirection: 'row', padding: 10, }, headerText: { fontSize: 16, fontWeight: '500', }, button: { backgroundColor: '#fff', padding: 10, borderRadius: 5, }, }); const postListStyles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f2f2f2', }, }); 
Enter fullscreen mode Exit fullscreen mode
  • (1) renderPostList: that control postList
  • (2) renderPostItem: control only logic to render post item => can add filter here, if not contain just return null => nothing show
  • (3) PostItem: control UI render for postItem => we can render PostItemOdd or PostItemEven if we want, this is very helpful if you try to think about it

Step1 Result

Step 2: Add filter condition

 export const FlatListDemo = () => { // add this const [keyword, setKeyword] = useState(''); const [order, setOrder] = useState<'ASC' | 'DESC'>('ASC'); const toggleOrder = () => { const newOrder = order === 'ASC' ? 'DESC' : 'ASC'; setOrder(newOrder); }; const postListFiltered = postList.filter(post => post.title.toLowerCase().includes(keyword.toLowerCase()), ); const postListSorted = postListFiltered.sort((a, b) => { if (order === 'ASC') { return a.title.localeCompare(b.title); } return b.title.localeCompare(a.title); }); const renderPostListHeader = () => { return ( <> <TextInput value={keyword} onChangeText={setKeyword} style={postListHeaderStyles.input} /> <TouchableOpacity style={styles.sortButton} onPress={toggleOrder}> <Text> Order: {order} - Total: {postListSorted.length} </Text> </TouchableOpacity> </> ); }; // and then  const renderPostList = () => { return ( <FlatList style={postListStyles.container} data={postListFiltered} ListHeaderComponent={renderPostListHeader()} // remember that we execute that function and return only the <></> renderItem={renderPostItem} keyExtractor={keyExtractor} /> ); }; // ...  }; const postListHeaderStyles = StyleSheet.create({ input: { backgroundColor: '#fff', padding: 10, margin: 10, borderRadius: 5, }, }); const styles = StyleSheet.create({ // ... sortButton: { backgroundColor: '#d2d2d2', padding: 10, borderRadius: 5, borderBottomColor: '#ddd', borderBottomWidth: 1, alignItems: 'flex-end', marginHorizontal: 10, }, }); 
Enter fullscreen mode Exit fullscreen mode

A1 - result

Remember to execute renderHeader function otherwise you can in trouble

Issues here https://github.com/facebook/react-native/issues/13365

 <FlatList style={postListStyles.container} data={postListSorted} ListHeaderComponent={renderPostListHeader()} renderItem={renderPostItem} keyExtractor={keyExtractor} /> 
Enter fullscreen mode Exit fullscreen mode

Step 3: Add highlight

 export const FlatListDemo = () => { // ... const [selectedIdList, setSelectedIdList] = useState<number[]>([]); const renderPostItem = ({item, index}: {item: Post; index: number}) => { // check by append and remove post console.log('renderPostItem', index); const highlight = selectedIdList.includes(item.id); return ( <PostItem index={index} item={item} highlight={highlight} onPress={() => { setSelectedIdList(curr => { const id = item.id; const newSelectedIdList = [...curr]; const i = newSelectedIdList.indexOf(id); if (i === -1) { newSelectedIdList.push(id); } else { newSelectedIdList.splice(i, 1); } return newSelectedIdList; }); }} /> ); }; // .. const renderPostListHeader = () => { return ( <> <TextInput value={keyword} onChangeText={setKeyword} style={postListHeaderStyles.input} /> <TouchableOpacity style={styles.sortButton} onPress={toggleOrder}> // add total selected <Text> Order: {order}. Total {postListSorted.length}. Selected{' '} {selectedIdList.length} </Text> </TouchableOpacity> </> ); }; }; // and then update PostItem const PostItem = React.memo( ({ item, index, onPress, highlight, }: { item: Post; index: number; onPress: (post: Post) => void; highlight: boolean; }) => { console.log('PostItem', index); return ( <TouchableOpacity style={[ postItemStyles.container, highlight && {backgroundColor: '#ffc701'}, ]} onPress={() => { onPress?.(item); }}> <Text style={postItemStyles.title}>{item.title}</Text> <Text style={postItemStyles.description}>{item.description}</Text> </TouchableOpacity> ); }, (prevProps, nextProps) => { // only re-render when item is changed // add one more condition to re-render when highlight return ( prevProps.item.id === nextProps.item.id && prevProps.highlight === nextProps.highlight ); }, ); 
Enter fullscreen mode Exit fullscreen mode

Image description

Step 4: Expand and Collapse Item

 import React, {useState} from 'react'; import { FlatList, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; interface Post { id: number; title: string; description: string; } const postMocks: Post[] = [ {id: 1, title: 'Post 1', description: 'Description for Post 1'}, {id: 2, title: 'Post 2', description: 'Description for Post 2'}, {id: 3, title: 'Post 3', description: 'Description for Post 3'}, {id: 4, title: 'Post 4', description: 'Description for Post 4'}, {id: 5, title: 'Post 5', description: 'Description for Post 5'}, {id: 6, title: 'Post 6', description: 'Description for Post 6'}, {id: 7, title: 'Post 7', description: 'Description for Post 7'}, {id: 8, title: 'Post 8', description: 'Description for Post 8'}, {id: 9, title: 'Post 9', description: 'Description for Post 9'}, {id: 10, title: 'Post 10', description: 'Description for Post 10'}, {id: 11, title: 'Post 11', description: 'Description for Post 11'}, {id: 12, title: 'Post 12', description: 'Description for Post 12'}, {id: 13, title: 'Post 13', description: 'Description for Post 13'}, {id: 14, title: 'Post 14', description: 'Description for Post 14'}, {id: 15, title: 'Post 15', description: 'Description for Post 15'}, {id: 16, title: 'Post 16', description: 'Description for Post 16'}, {id: 17, title: 'Post 17', description: 'Description for Post 17'}, {id: 18, title: 'Post 18', description: 'Description for Post 18'}, {id: 19, title: 'Post 19', description: 'Description for Post 19'}, {id: 20, title: 'Post 20', description: 'Description for Post 20'}, {id: 21, title: 'Post 21', description: 'Description for Post 21'}, {id: 22, title: 'Post 22', description: 'Description for Post 22'}, {id: 23, title: 'Post 23', description: 'Description for Post 23'}, {id: 24, title: 'Post 24', description: 'Description for Post 24'}, {id: 25, title: 'Post 25', description: 'Description for Post 25'}, {id: 26, title: 'Post 26', description: 'Description for Post 26'}, {id: 27, title: 'Post 27', description: 'Description for Post 27'}, {id: 28, title: 'Post 28', description: 'Description for Post 28'}, {id: 29, title: 'Post 29', description: 'Description for Post 29'}, {id: 30, title: 'Post 30', description: 'Description for Post 30'}, ]; const PostItem = React.memo( ({ item, index, toggleSelect, highlight, toggleExpand, expand, }: { item: Post; index: number; toggleSelect: (post: Post) => void; toggleExpand: (post: Post) => void; highlight: boolean; expand: boolean; }) => { console.log('PostItem', index); return ( <View style={postItemStyles.wrapper}> <TouchableOpacity style={[ postItemStyles.container, highlight && {backgroundColor: '#ffc701'}, ]} onPress={() => { toggleSelect?.(item); }}> <Text style={postItemStyles.title}>{item.title}</Text> {/* <Text style={postItemStyles.description}>{item.description}</Text> */} </TouchableOpacity> <TouchableOpacity style={postItemStyles.expandButton} onPress={() => { toggleExpand?.(item); }}> <Text>{expand ? 'Collapse' : 'Expand'}</Text> </TouchableOpacity> </View> ); }, (prevProps, nextProps) => { // only re-render when item is changed return ( prevProps.item.id === nextProps.item.id && prevProps.highlight === nextProps.highlight && prevProps.expand === nextProps.expand ); }, ); const PostItemExpanded: React.FC<{ item: Post; index: number; }> = ({item, index}) => { console.log('PostItemExpanded', index); return ( <View style={[postItemStyles.container]}> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> <Text style={postItemStyles.description}>{item.description}</Text> </View> ); }; const postItemStyles = StyleSheet.create({ wrapper: { flexDirection: 'row', }, container: { backgroundColor: '#fff', padding: 10, marginBottom: 1, flex: 1, }, title: { fontSize: 16, fontWeight: 'bold', }, description: { fontSize: 14, marginTop: 10, }, expandButton: { backgroundColor: '#9ad0dc', padding: 10, justifyContent: 'center', alignItems: 'center', }, }); export const FlatListDemo = () => { const [postList, setPostList] = useState(postMocks); const [keyword, setKeyword] = useState(''); const [order, setOrder] = useState<'ASC' | 'DESC'>('ASC'); const [selectedIdList, setSelectedIdList] = useState<number[]>([]); const [expandedIdList, setExpandedIdList] = useState<number[]>([]); /** * create renderPostItem: => can reduce anonymous function in renderPostList * anonymous function will be created every time renderPostList is called => so it's better to create a function outside * @param param0 * @returns */ const renderPostItem = ({item, index}: {item: Post; index: number}) => { // alway re-render each time renderPostList re-render // to reduce re-render UI, we can use React.memo to create new component that only handle UI // check by append and remove post console.log('renderPostItem', index); const highlight = selectedIdList.includes(item.id); const expand = expandedIdList.includes(item.id); if (index % 2 === 1) { if (expand) { return <PostItemExpanded item={item} index={index} />; } return null; } else { return ( <PostItem index={index} item={item} highlight={highlight} toggleSelect={() => { setSelectedIdList(curr => { const id = item.id; const newSelectedIdList = [...curr]; const i = newSelectedIdList.indexOf(id); if (i === -1) { newSelectedIdList.push(id); } else { newSelectedIdList.splice(i, 1); } console.log('setSelectedIdList', curr, newSelectedIdList); return newSelectedIdList; }); }} toggleExpand={() => { setExpandedIdList(curr => { const id = item.id; const newExpandedIdList = [...curr]; const i = newExpandedIdList.indexOf(id); if (i === -1) { newExpandedIdList.push(id); } else { newExpandedIdList.splice(i, 1); } console.log('setExpandedIdList', curr, newExpandedIdList); return newExpandedIdList; }); }} expand={expand} /> ); } }; /** * * @param item * @returns */ const keyExtractor = (item: Post, index: number) => `${item.id}-${index}`; const appendPost = () => { const newPost = { id: postList.length + 1, title: `Post ${postList.length + 1}`, description: `Description for Post ${postList.length + 1}`, }; setPostList([...postList, newPost]); }; const removeLastPost = () => { const newPostList = [...postList]; newPostList.pop(); setPostList(newPostList); }; const toggleOrder = () => { const newOrder = order === 'ASC' ? 'DESC' : 'ASC'; setOrder(newOrder); }; const postListFiltered = postList.filter(post => post.title.toLowerCase().includes(keyword.toLowerCase()), ); const postListSorted = postListFiltered.sort((a, b) => { if (order === 'ASC') { return a.title.localeCompare(b.title); } return b.title.localeCompare(a.title); }); const duplicateListSorted: Post[] = []; for (const post of postListSorted) { duplicateListSorted.push(post); duplicateListSorted.push(post); } const renderPostListHeader = () => { return ( <> <TextInput value={keyword} onChangeText={setKeyword} style={postListHeaderStyles.input} /> <TouchableOpacity style={styles.sortButton} onPress={toggleOrder}> <Text> Order: {order}. Total {postListSorted.length}. Selected{' '} {selectedIdList.length}. Expanded {expandedIdList.length} </Text> </TouchableOpacity> </> ); }; // stickyHeaderIndices = odd of duplicateListSorted const stickyHeaderIndices = duplicateListSorted .map((_, index) => index) .filter(index => index % 2 === 1); const renderPostList = () => { return ( <FlatList style={postListStyles.container} data={duplicateListSorted} ListHeaderComponent={renderPostListHeader()} renderItem={renderPostItem} keyExtractor={keyExtractor} stickyHeaderIndices={stickyHeaderIndices} /> ); }; return ( <View style={styles.container}> <View style={styles.header}> {/* appendPost */} <TouchableOpacity onPress={appendPost} style={styles.button}> <Text>Append Post</Text> </TouchableOpacity> {/* removeLastPost */} <TouchableOpacity onPress={removeLastPost} style={styles.button}> <Text>Remove Last Post</Text> </TouchableOpacity> </View> {renderPostList()} </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', borderTopColor: '#ddd', borderTopWidth: 1, }, header: { backgroundColor: '#ddd', justifyContent: 'space-between', alignItems: 'center', flexDirection: 'row', padding: 10, }, headerText: { fontSize: 16, fontWeight: '500', }, button: { backgroundColor: '#fff', padding: 10, borderRadius: 5, }, sortButton: { backgroundColor: '#d2d2d2', padding: 10, borderRadius: 5, borderBottomColor: '#ddd', borderBottomWidth: 1, alignItems: 'flex-end', marginHorizontal: 10, }, }); const postListStyles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f2f2f2', }, }); const postListHeaderStyles = StyleSheet.create({ input: { backgroundColor: '#fff', padding: 10, margin: 10, borderRadius: 5, }, }); 
Enter fullscreen mode Exit fullscreen mode

Final Result

Issues

  • When stickyHeaderIndices update => flatlist will force update and re-render everything => this is cause an interrupt when you type => Not have any solution for it => Final result must remove stickyHeaderIndices

Top comments (0)