mr3mo
mr3mo

Reputation: 145

React Native FlatList makes app extremely slow after 10 elements

I am trying to build a simple stopwatch app in react-native. I'm using AsyncStorage to store the time recorded data into local storage, along with that I would like to display a table that shows all the recorded times. The core idea is that when a person presses and holds a LottieView animation, it will start a timer, when they press out, the timer stops, records in AsyncStorage and then updates the table.

After 10 elements, my FlatList (inside TimeTable.jsx) becomes extremely slow and I am not sure why. The component that is causing this error is I believe TimeTable.jsx but I am not quite sure why.

src/components/Timer/TimeTable.jsx

import React, {useState, useEffect} from 'react'
import { StyleSheet, FlatList  } from "react-native";
import { Divider, List, ListItem } from '@ui-kitten/components'
import AsyncStorage from '@react-native-async-storage/async-storage';

const getRecordedEventsTable = async (dbKey) => {
  try {
    let currentDataArray = await AsyncStorage.getItem(dbKey);
    return currentDataArray ? JSON.parse(currentDataArray) : [];
  } catch (err) {
    console.log(err);
  }
};

const renderItem = ({ item, index }) => (
  <ListItem
  title={`${item.timeRecorded / 1000} ${index + 1}`}
  description={`${new Date(item.timestamp)} ${index + 1}`}
   />
)

export const TimeTable = ({storageKey, timerOn}) => {
  const [timeArr, setTimeArr] = useState([]);

  useEffect(() => {
    getRecordedEventsTable(storageKey).then((res) => {
        setTimeArr(res)
    })

  }, [timerOn])

  return (
      <FlatList
        style={styles.container}
        data={timeArr}
        ItemSeparatorComponent={Divider}
        renderItem={renderItem}
        keyExtractor={item => item.timestamp.toString()}
      />
  );
};

const styles = StyleSheet.create({
    container: {
      maxHeight: 200,
    },
  });

src/components/Timer/Timer.jsx

import React, {useState, useEffect, useRef} from 'react'
import {
    View,
    StyleSheet,
    Pressable,
} from 'react-native';
import {Layout, Button, Text} from '@ui-kitten/components';
import LottieView from 'lottie-react-native'
import AsyncStorage from '@react-native-async-storage/async-storage';
import {TimeTable} from './TimeTable'

const STORAGE_KEY = 'dataArray'

const styles = StyleSheet.create({
    container: {
        flex: 1, 
        justifyContent: "center",
        alignItems: "center",
        backgroundColor: "#E8EDFF"
    },
    seconds: {
        fontSize: 40,
        paddingBottom: 50,
    }
})

const getRecordedEventsTable = async () => {
    try {
        let currentDataArray = await AsyncStorage.getItem(STORAGE_KEY)
        return currentDataArray ? JSON.parse(currentDataArray) : []
    } catch (err) {
        console.log(err)
    }
}

const addToRecordedEventsTable = async (item) => {
    try {
        let dataArray = await getRecordedEventsTable()
        dataArray.push(item)
    
        await AsyncStorage.setItem(
            STORAGE_KEY,
            JSON.stringify(dataArray)
            )
    } catch (err) {
        console.log(err)
    }
}

// ...

const Timer = () => {
    const [isTimerOn, setTimerOn] = useState(false)
    const [runningTime, setRunningTime] = useState(0)
    const animation = useRef(null);

    const handleOnPressOut = () => {
        setTimerOn(false)
        addToRecordedEventsTable({
            timestamp: Date.now(),
            timeRecorded: runningTime
        })

        setRunningTime(0)
    }

     useEffect(() => {
        let timer = null

        if(isTimerOn) {
            animation.current.play()

            const startTime = Date.now() - runningTime
            timer = setInterval(() => {
                setRunningTime(Date.now() - startTime)
            })
        } else if(!isTimerOn) {
            animation.current.reset()
            clearInterval(timer)
        }

        return () => clearInterval(timer)
    }, [isTimerOn])

    return (
        <View>
            <Pressable onPressIn={() => setTimerOn(true)} onPressOut={handleOnPressOut}>
                <LottieView ref={animation} style={{width: 300, height: 300}} source={require('../../../assets/record.json')} speed={1.5}/>
            </Pressable>
            <Text style={styles.seconds}>{runningTime/1000}</Text>
            <TimeTable storageKey={STORAGE_KEY} timerOn={isTimerOn} />
            <Button onPress={resetAsyncStorage}>Reset Async</Button>
        </View>
    )
}

export default Timer

Any help, appreciated. Thanks.


EDIT: Received the following warning in console:

VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
  "contentLength": 1362.5,
  "dt": 25161,
  "prevDt": 368776,

EDIT: In Timer.jsx, I have a Text View in the render function as follows: <Text style={styles.seconds}>{runningTime/1000}</Text>, this part is supposed to show the stopwatch value and update with the timer. As the FlatList gets bigger, this is the part that becomes extremely laggy.

My suspicion is that as this is trying to re-render constantly, the children component TimeTable.jsx is also re-rendering constantly?

Upvotes: 0

Views: 3735

Answers (3)

mr3mo
mr3mo

Reputation: 145

I was able to solve this problem. The main culprit for the slowness was that in the parent component Timer.jsx because the timerOn props is changing everytime the user presses the button, the whole children component is trying to re-render and that AsyncStorage call is being called everytime. This is the reason that the {runningTime/1000} is rendering very slowly. Because everytime the timerOn component changes all child components have been queued to re-render.

The solution for this was to render the Table component from a parent of Timer and not inside the Timer component and maintain a state in Timer which is passed back to the parent and then passed to the Table component.

This is what my parent component looks like now:

  const [timerStateChanged, setTimerStateChanged] = useState(false);

  return (
    <View style={styles.container}>
      <Timer setTimerStateChanged={setTimerStateChanged} />
      <View
        style={{
          borderBottomColor: "grey",
          borderBottomWidth: 1,
        }}
      />
      <TimeTable timerOn={timerStateChanged} />
    </View>
  );
};

A better way would be to use something like React context or Redux.

Thanks for all the help.

Upvotes: 0

awan_u
awan_u

Reputation: 21

For optimizing the FlatList you can use different parameters that are available. You can read this https://reactnative.dev/docs/optimizing-flatlist-configuration.

Also you might consider using useCallback hook for renderItems function.

I would recommend reading this https://medium.com/swlh/how-to-use-flatlist-with-hooks-in-react-native-and-some-optimization-configs-7bf4d02c59a0

Upvotes: 0

jnpdx
jnpdx

Reputation: 52337

Looks to me like you have a loop here:

useEffect(() => {
    getRecordedEventsTable(storageKey).then((res) => {
        setTimeArr(res)
    })

  }, [timeArr, timerOn])

useEffect will get called every time timeArr is updated. Then, inside you call your async getRecordedEventsTable, and every time that finishes, it'll call setTimeArr, which will set timeArr, triggering the sequence to start again.

Upvotes: 1

Related Questions