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?
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.
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
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.
Related
I've been tring to load a list of items from a database onto a FlatList, but the FlatList keeps loading repetedly indefinetly.
Say the list contains only 10 items - it will load the 10, then start again from 1 - 10, over and over.
How can I prevent this and only load the 10 items only once?
Thank you all in advance.
Here's how I'm going about it :
import {View, FlatList} from 'react-native';
import React, {useState} from 'react';
export const MyFunctionalComponent = () => {
[dBList, setDBList] = useState(null);
let getMyDbList = () => {
return getDbList();
};
new Promise((res, rej) => {
let myDbList = getMyDbList();
res(myDbList);
}).then(result => {
setDBList(result);
});
const renderItem = ({item}) => {
return (
<View key={item.myGUID.toString()} />
);
};
return (
<View>
{dBList && (
<FlatList
data={dBList}
renderItem={renderItem}
keyExtractor={item => {
item.myGUID.toString();
}}
/>
)}
</View>
);
};
Because your promise declares in the root level, which will be executed every time the component is rendered. Just move this to useEffect to load only once
useEffect(() => {
new Promise((res, rej) => {
let myDbList = getMyDbList();
res(myDbList);
}).then(result => {
setDBList(result);
});
}, [])
I'm trying out React Native by implementing a simple timer. When running the code, the counting works perfectly for the first 6 seconds, where then the app starts to act weird.
Here is the code, which you can try in this Expo Snack
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native';
const App = () => {
return (
<View style={styles.container}>
<Timer></Timer>
</View>
);
}
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
For each new component rerender, React create a new interval, which results in a memory leak and unexpected behavior.
Let us create a custom hook for interval
import { useEffect, useLayoutEffect, useRef } from 'react'
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
// Remember the latest callback if it changes.
useLayoutEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
// Don't schedule if no delay is specified.
// Note: 0 is a valid value for delay.
if (!delay && delay !== 0) {
return
}
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
export default useInterval
Use it in functional component
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
useInterval(()=>{
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
},1000)
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
Here tested result - https://snack.expo.dev/#emmbyiringiro/58cf4b
As #AKX commented, try wrapping your interval code in a useEffect. Also, return a function from it that clears the interval (the cleanup function).
useEffect(() => {
const interval = setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return () => clearInterval(interval);
});
However, using setInterval with 1000 second delay does not yield an accurate clock, if that's what you're going for.
I have been trying to achieve hiding (or slide up) my react native startup app top bar after 3 seconds, then have a button to press to show the top bar, but could not. I've searched online how to do it, but have not seen good solution for it. Please I need help here, the below is snippet code of what I tried doing but it did not work.
<HeaderHomeComponent /> is the top header I created and imported it in my Home screen
const Home = () => {
const camera = useRef();
const [isRecording, setIsRecording] = useState(false);
const [flashMode, setFlashMode] = useState(RNCamera.Constants.FlashMode.off);
const [cameraType, setCameraType] = useState(RNCamera.Constants.Type.front);
const [showHeader, setShowHeader] = useState(true);
const onRecord = async () => {
if (isRecording) {
camera.current.stopRecording();
} else {
setTimeout(() => setIsRecording && camera.current.stopRecording(), 23*1000);
const data = await camera.current.recordAsync();
}
};
setTimeout(()=> setShowHeader(false),3000);
const DisplayHeader = () => {
if(showHeader == true) {
return <HeaderHomeComponent />
} else {
return null;
}
}
// useEffect(() => {
// DisplayHeader();
// }, [])
return (
<View style={styles.container}>
<RNCamera
ref={camera}
type={cameraType}
flashMode={flashMode}
onRecordingStart={() => setIsRecording(true)}
onRecordingEnd={() => setIsRecording(false)}
style={styles.preview}
/>
<TouchableOpacity
style={styles.showHeaderButton}
onPress={() => {
setShowHeader(!showHeader);
}}>
<Button title='Show' />
</TouchableOpacity>
<HeaderHomeComponent />
You were really close.
This is how it should be done:
useEffect(() => {
setTimeout(toggleHeader,3000);
}, []);
const toggleHeader = () => setShowHeader(prev => !prev);
Then inside the "return":
{showHeader && <HeaderHomeComponent />}
As simple as that.
This should help you get started in the right direction, you can use animation based on your preference to this code.
import React, {useState, useEffect} from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import Constants from 'expo-constants';
export default function App()
{
const [showStatusbar, setShowStatusbar] = useState(true)
useEffect(() =>
{
setTimeout(() =>
{
setShowStatusbar(false)
}, 3000)
}, [])
return (
<View style={styles.container}>
{
showStatusbar
? <View style = {styles.statusBar}>
<Text>Status Bar</Text>
</View>
: null
}
<Button title = "Show Status bar" onPress = {() => setShowStatusbar(true)}/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ecf0f1',
},
statusBar:
{
height: 50,
backgroundColor:'lightblue',
justifyContent:'center',
alignItems:'center'
}
});
Instead of setTimeout use Animation.
or
Check this lib : https://www.npmjs.com/package/react-native-animated-hide-view
or
check below answers which might help you
Hide and Show View With Animation in React Native v0.46.
TL;DR
When a store change triggers a component function, the current component state is ignored/reset, not letting me use its state data to feed the triggered function.
Full Description
This react-native app has a button located in a heading Appbar stack navigator, which must trigger a function that the currently focused Screen has.
The thing is that this screen is very deep within the navigation scheme, thus I decided to use Redux to directly notify the screen that the button has been pressed.
This also means that every time that this button is pressed and a store slice gets dispatched, I can trigger any function only depending on the Screen implementation.
If i use the very same function from a button within the component it works perfectly. However if I call the same function from the redux store change i get this log:
Console Behavior
# component loaded
false
# started writing, this is the component state
h
he
hel
hell
hello
#header button 'create' state change detected
true
#content as viewed by the onPressPublish function
content: ""
Error 400 - Cannot save empty content
#store reset for further use
false
Appbar
export const AppBarStackNavigator = (props) => {
const { toggle } = useSelector(toolbarSelector);
const handleCreatePress = () => {
dispatch(setCreate({ pressed: true }));
}
return (
<Appbar.Header
style={{ backgroundColor: theme.colors.background, elevation: 0 }}
>
<Button
icon="seed"
mode="contained"
// disabled={!contentProps.valid}
onPress={handleCreatePress}
labelStyle={{ color: 'white' }}
style={{
width: 115,
borderRadius: 50,
alignSelf: "flex-end"
}}>
Sembrar
</Button>
</Appbar.Header>
);
}
Store
import { createSlice } from "#reduxjs/toolkit";
export const toolbarSlice = createSlice({
name: 'toolbar',
initialState: {
create: false
},
reducers: {
setCreate(state, action) {
state.create = action.payload.pressed;
}
}
})
export const { setCreate } = toolbarSlice.actions;
export const toolbarSelector = state => state.toolbar
export default toolbarSlice.reducer;
The navigationally-deep component
import { toolbarSelector, setCreate } from '../store/toolbar';
import { useSelector } from 'react-redux';
// import { useFocusEffect, TabActions, useNavigation } from '#react-navigation/native';
export const DeepComponent = (props) => {
const theme = useTheme();
const { create } = useSelector(toolbarSelector);
return (
<ChildComponent {...props} create={create} setCreate={setCreate} style={{ backgroundColor: theme.colors.background }} />
);
};
Its child (where the function is)
import { useDispatch, useSelector } from 'react-redux';
export const ChildComponent = (props) => {
const dispatch = useDispatch();
const [content, setContent] = useState(''));
let payload = {};
const onPressPublish = async () => {
try {
console.log(payload);
payload = {
...payload,
content,
// images <- other component states
}
console.log(payload);
const seed = await api.writeOne(payload);
} catch (error) {
console.log(error);
Alert.alert('Could not publish :(', error.message);
}
navigation.goBack();
}
useEffect(() => {
console.log(props.create)
console.log(content)
if (props.create) {
console.log(content)
onPressPublish();
}
return () => {
dispatch(props.setCreate({ pressed: false }));
};
}, [props.create])
const onTextChange = (value, props) => {
// ...
setContent(value);
// ...
}
return (
<TextInput
mode='flat'
placeholder={inputPlaceholder}
multiline
onChangeText={text => onTextChange(text, props)}
keyboardShouldPersistTaps={true}
autoFocus
clearButtonMode='while-editing'>
<ParsedContent content={content} />
</TextInput>
<Button
disabled={!contentProps.valid}
onPress={onPressPublish}>
{buttonText}
</Button>
)
}
Here are some suggestions to change the code, you still have not provided any code in your question that would make sense (like the payload variable) but this may give you an idea where to go.
When you create an app with create-react-app you should have a linter that tells you when you have missing dependencies in hooks, you should not ignore these warnings:
//create onPressPublish when component mounts
const onPressPublish = useCallback(async (content) => {
try {
//removed payload as it makes no sense to create it
// and never use it anywhere
//removed assigning to seed becasue it is never used anywhere
await api.writeOne({ content });
} catch (error) {
console.log(error);
Alert.alert('Could not publish :(', error.message);
}
navigation.goBack();
}, []);
const { create, setCreate } = props;
useEffect(() => {
if (create) {
//passing content
onPressPublish(content);
}
return () => {
dispatch(setCreate({ pressed: false }));
};
}, [
//correct dependencies without linter warnings
content,
dispatch,
onPressPublish,
create,
setCreate,
]);
I have an array of objects and this data should be stored in when new item is appended to the list. Also, when the application loads, this information have to be pushed back into the list. So, I use AsyncCallback, but it doesn't work properly. When I refresh application, all stored elements appear in the list except the last one. How can I return this item back to the list?
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, FlatList, Button, AsyncStorage } from 'react-native';
export default function HomeScreen() {
const [listItems, setListItems] = useState([
{
// example list item
id: '0',
text: 'Item 1'
},
]);
const [idx, incrIdx] = useState(1);
useEffect(() => {
getData();
}, []);
const pushItem = () => {
var data = new Object();
data.id = idx.toString();
data.text = "Item " + idx;
setListItems([...listItems, data]);
incrIdx(idx + 1);
storeData();
};
const storeData = async () => {
await AsyncStorage.setItem('listItems', JSON.stringify(listItems));
};
const getData = async () => {
const value = await AsyncStorage.getItem('listItems');
if(value !== null) {
setListItems([...listItems, JSON.parse(value)]);
}
};
return (
<View style={styles.container}>
<FlatList
data={listItems}
renderItem={({item}) =>
<View>
<Text>{item.text}</Text>
</View>
}
keyExtractor={item => item.id}
/>
<Button
title="Push data"
onPress={pushItem}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
Did you look at what AsyncStorage.setItem is actually setting?
My guess is that it's not using the latest listItems state because hook setters are asynchronous (just like the old setState).
To express logic that should take effect when state is updated, use the useEffect api.
useEffect(() => {
// Instead of calling storeData() with stale state
AsyncStorage.setItem('listItems', JSON.stringify(listItems));
}, [listItems]);