DEV Community

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

Posted on • Edited on

14

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

Sentry mobile image

Improving mobile performance, from slow screens to app start time

Based on our experience working with thousands of mobile developer teams, we developed a mobile monitoring maturity curve.

Read more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay