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',
},
});
- (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
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,
},
});
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}
/>
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
);
},
);
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,
},
});
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)