react-nativereact-native-flatlistflatlist

React Native - Flatlist Handling Large Data


Problem : I'm developing a chat app and i was trying to render items like whatsapp does because its the most important thing , never show not loaded data to user. As you know whatsapp can render the whole messages like instantly (probably by using 'onEndReach'). I have thousands and hundreds of datas in a flatlist and was too slow about rendering. I tried the react native's documentation here https://reactnative.dev/docs/optimizing-flatlist-configuration it increase the performance a little bit but not as i expected. Because i want to render that data much faster so user can't catch the load speed and see the blank things. Also i searched too many web sites but doesn't find anything about it as well documented and clear. So i decided to share my solution here, hopefully it helps who suffers from that.


Solution

  • My Solution: Pagination was the most useful thing for performance. If you load little data its fast but suffers from the big datas then it will help i think. I tried many libraries but most of them has no dynamic height calculation. Flatlist comes with many good feature like that. I compared many of them and this solution was the best comparing to development time and performance actually. Currently i have more than 1000 number of data which contains audio messages, images etc. If all of your components are images then the react-native suggest Fast-Image component. I didn't try that case for now. See the code =>

    
    function ChatRoomScreen() {
    
    const [offset, setOffset] = useState(1); //Its Like Page number
    const [ messages, setMessages ] = useState<[]>([]); //Contains the whole data
    const [dataSource, setDataSource] =  useState<[]>([]); //Contains limited number of data
    const renderMessage =  function({ item }) { //Your render component
        return <Message/>
    };
    const keyExtractor = (item) => item.id;
    const windowSize =messages.length > 50 ? messages.length/4 : 21;
    let num =100 // This is the number which defines how many data will be loaded for every 'onReachEnd'
    let initialLoadNumber = 40 // This is the number which defines how many data will be loaded on first open
    
    useEffect(() => { //Initially , we set our data.
    
        setMessages('YOUR WHOLE ARRAY OF DATA HERE'); 
    
    }, [])
    
    useEffect(()=> { //Here we setting our data source on first open.
    
        if(dataSource.length < messages.length){  
            if(offset == 1){
                setDataSource(messages.slice(0,offset*initialLoadNumber ))
            }      
        }
    
    }, [messages]);  
    
    const getData = () => { // When scrolling we set data source with more data.
    
        if(dataSource.length < messages.length && messages.length != 0){
            setOffset(offset + 1);
            setDataSource(messages.slice(0,offset*num )) //We changed dataSource.
        }
    
    };
    
    return(
            <SafeAreaView style={styles.page}>
                {messages.length!=0 && <FlatList
                        data={dataSource}
                        renderItem={renderMessage}
                        inverted
                        initialNumToRender={initialLoadNumber}
                        windowSize={windowSize} //If you have scroll stuttering but working fine when 'disableVirtualization = true' then use this windowSize, it fix the stuttering problem.
                        maxToRenderPerBatch={num}
                        updateCellsBatchingPeriod={num/2}
                        keyExtractor={keyExtractor}
                        onEndReachedThreshold ={offset < 10 ? (offset*(offset == 1 ? 2 : 2)):20} //While you scolling the offset number and your data number will increases.So endReached will be triggered earlier because our data will be too many
                        onEndReached = {getData} 
                        removeClippedSubviews = {true}    
                    /> 
                }       
            </SafeAreaView>
        )
    };
    
    export default ChatRoomScreen
    
    

    Also don't forget to do this in your render component =>

    function arePropsEqual(prevProps, nextProps) {
      return prevProps.id === nextProps.id; //It could be something else not has to be id.
    }
    export default memo(Message,arePropsEqual); //Export with memo of course :) 
    

    If you dont check this, when you data change then your whole data will be re-rendered every time you want to add more to render.

    With that , my messages loaded like whatsapp even if the components are heavy. I scrolled too fast and there was no blank fields. Maybe you can see some blanks if you're using expo in development mode but that's not happened to me. If you experienced this then i suggest you give a try on production mode, it much more faster on standalone app and was impossible to catch the data load speed with scrolling as i see. So the main logic here we never gave the whole data to flatlist, we do some kind of pagination here and it worked for me ! If you try this and have a good idea about it please share and we discuss. Because that flatlist thing is not very good when you use it on default thats why as i see people uses other libraries instead of react's own flatlist.

    Update : I used this for load more instead of getting whole data in an array. Now it dynamically concat the incoming data to my array

    await DataStore.query(MessageModel, 
                        message => message.chatroomID("eq", chatRoom?.id),
                        {
                            sort: message => message.createdAt(SortDirection.DESCENDING),
                            page:offset,
                            limit:num,
                        }
                        
                    ).then((e)=>{
                        setMessages(messages.concat(e));
                    });
    

    Info : You can see this in chatroomscreen function ->

    const renderMessage =  function({ item }) { //Your render component
        return <Message/> // Message is a component which is imported from another file.
    };
    

    Here is the 'Message' component. You can see how i exported and used 'arePropsEqual' function. Message component is a component to actually render in my chat screen. Chat screen is just a page to get correct datas then visualize them to user by using other components.

    -> Message.tsx component

    import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
    import React, {useState, useEffect, memo} from 'react';
    import { DataStore, Storage } from 'aws-amplify';
    import {S3Image} from 'aws-amplify-react-native';
    import AudioPlayer from '../AudioPlayer';
    import { Ionicons } from '@expo/vector-icons'; 
    import { Message as MessageModel } from '../../src/models';
    import moment from 'moment';
    //import FastImage from 'react-native-fast-image'
    
    const blue = '#3777f0';
    const grey = 'lightgrey';
    
    const Message = ( props ) => {
    
      const [message, setMessage] = useState<MessageModel>(props.message);
      const [user, setUser] = useState<string|null>(null);
      const [isMe, setIsMe] = useState<boolean | null>(true);
      const [soundURI, setSoundURI] = useState<string|null>(null);
      const [myUser, setMyUser] = useState<string|undefined>();
      
       useEffect(()=> {
        setUser(props.userid)
        setMyUser(props.myID);
      }, []); 
    
      useEffect(() => {
        const subscription = DataStore.observe(MessageModel, message.id).subscribe((msg) => {
            if(msg.model === MessageModel && msg.opType === 'UPDATE'){
                if(msg.element.content){
                 
                  setMessage(msg.element)
                }
            
            }
        });
        return () => subscription.unsubscribe();
      }, [])
    
      useEffect(() => {
        setAsRead()
      },[isMe, message])
    
      useEffect(() => {
        if(message.audio){
          Storage.get(message.audio).then(setSoundURI);
        }
    
      },[message])
    
      useEffect(()=> {
        const checkIfMe = async () => {
          if (!user){
            return;
          }
          setIsMe(user === myUser)
        }
        checkIfMe();
      }, [user]);
    
      const setAsRead = async () => {
        if(isMe === false && message.status !== 'READ'){
          await DataStore.save(MessageModel.copyOf(message, (updated) => {
              updated.status = 'READ';
          }));
        }
      }
    
      if(!user){
        return <ActivityIndicator/>
      }
    
      return (
        
        <View style={[
            styles.container,
            isMe ? styles.rightContainer : styles.leftContainer,
            {width: soundURI ? '75%':'auto',height:'auto'}]}>
               {message.image && (
                  <View style = {{marginBottom: message.content ? 10 : 0 }}>
                     <S3Image 
                      imgKey={message.image} 
                      style = {{ aspectRatio: 4/4}}
                      resizeMode = 'contain'     
                    /> 
                  </View>
                
              )}
             {soundURI && (<AudioPlayer soundURI={soundURI}/>)}
            <View style = {{flexDirection:'column',justifyContent:'space-between'}} >
                
                <View style = {{justifyContent: !!message.content ?'space-between':'flex-end',flexDirection:'row'}}>
                  {!!message.content && <Text style={{color: isMe ? 'black' : 'white'}}>{message.content}</Text>}
                 
                </View>
                <View style= {styles.checkAndHour}>
                  <Text style = {{fontSize:12,color:isMe? '#a6a6a6':'#dbdbdb'}}>{moment(message.createdAt).format('HH:mm')}</Text>
                  {isMe &&  !!message.status && 
                        <View >   
                          <Ionicons 
                                name={message.status === 'SENT' ? "checkmark" : 'checkmark-done' } 
                                size={16} color={message.status === 'READ' ? "blue" : "gray"} 
                                style ={{marginLeft:2}}
                              />
                        </View>
                    } 
                  </View>
            </View> 
        
        </View>
      );
    };
    
    const styles = StyleSheet.create({
        container: {
            padding: 10,
            margin: 10,
            borderRadius: 10,
            maxWidth:'75%',   
            
        },
        leftContainer: {
            backgroundColor: blue,
            marginLeft: 10,
            marginRight: 'auto',
            
        },
        rightContainer: {
            backgroundColor: grey,
            marginLeft: 'auto',
            marginRight: 10,
            
        },
        checkAndHour: {
          marginLeft:20,
          alignItems:'flex-end',
          alignSelf:'flex-end',
          flexDirection:'row',  
          right: 0,
          bottom:0
          
        },
    
    })
    
    // Here it is >>>
    function arePropsEqual(prevProps, nextProps) {
      return prevProps.id === nextProps.id; 
    }
    
    export default memo(Message,arePropsEqual);