How to reder page only after await function - javascript

I have the following code ..
const WalletScreen = () => {
const checkBalance = async () => {
const docRef = doc(db, "users", auth.currentUser.uid);
const docSnap = await getDoc(docRef);
let tempBalance;
if (docSnap.exists()) {
const notFirstRun = docSnap.data().notFirstRun;
if (notFirstRun) {
Wbalance = docSnap.data().WalletBalance;
} else {
tempBalance = 1000000;
await setDoc( docRef,
{ WalletBalance: tempBalance, notFirstRun: true },
{ merge: true }
).catch((error) => {
alert(error.message);
});
}
}
return tempBalance;
};
let displayBalance = checkBalance();
return (
<View style={{ backgroundColor: "white", alignItems: "center" }}>
<Text style={{ fontSize: 42, marginVertical: 30 }}>
K{(displayBalance / 100)}
</Text>
</View>
);
};
I have a function checkBalance where I am trying to show an amount on the screen. If it is the first run of the app, the balance(stored in tempBalance) is set to 1000000 and saved in the firestore database, if it isn't the first run however, the balance should be retrieved from Firestore. At the end there is variable displayBalance that is to be used to display the balance. I thought using async await will mean all this will happen before the page is rendered, however it displays NAN like in the photo attached. Could anyone point out where I have gone wrong?

I believe the problem comes from your await syntax not affecting the scope containing the part you want to stall. To solve this with react mentality, I use the useEffect hook, and have a function which runs once on the initial load, and change a loading state to determine what to render. This idea, of having different states render depending on the content recieved from a database or some function execution could be repurposed for your use case. Hopefully this helps shed some light on solving your problem!
const {useState, useEffect} = React;
const AfterAsync = () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const wait = new Promise(r => setTimeout(r, 1000));
wait.then(() => setIsLoading(false));
}, []);
if (isLoading) return (
<div> Loading </div>
);
return (
<div> finished loading! </div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<AfterAsync />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

Related

Firebase only fetches data on second or third attempt

I'm building an app with React and Firestore.
In one feature, I need to use some specific user data to query data from another collection, and show that on the app.
Specifically I want to use users.books, which returns an array, to query the collection books.
However, for some reason the users.books doesn't load on first render. It typically takes 2-3 renders to fetch the books.user data. This is despite the currentUserUID being loaded right away.
I've tried using a loading state as specified in How to wait for Firebase data to be fetched before progressing?, but to no avail.
Do I need to use the onSnapShot method?
Thanks for reading
My code
import 'firebase/firestore'
import { booksRef} from '../../App';
const ProfileScreen = ({ navigation }) => {
const currentUserUID = firebase.auth().currentUser.uid;
const [firstName, setFirstName] = useState('');
const [userBookTitles, setUserBookTitles] = useState([]);
const [userBooks, setUserBooks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function getUserInfo(){
let doc = await firebase
.firestore()
.collection('users')
.doc(currentUserUID)
.get();
if (!doc.exists){
Alert.alert('No user data found!')
} else {
let dataObj = doc.data();
setFirstName(dataObj.firstName)
setUserBookTitles(dataObj.books)
console.log(userBookTitles)
}
}
getUserInfo();
}, [])
useEffect(() => {
async function getUserBooks() {
booksRef.where("title", "in", userBookTitles).onSnapshot(snapshot => (
setUserBooks(snapshot.docs.map((doc) => ({id: doc.id, ...doc.data()})))
))
}
setLoading(false);
getUserBooks()
}, [])
if (!loading) {
return (
<View style={styles.container}>
<Text> Hi {firstName} </Text>
<TouchableOpacity onPress={handlePress}>
<Text> Log out </Text>
</TouchableOpacity>
<Row
books={userBooks}
/>
</View>
);
} else {
return (
<View style={styles.container}>
<Text> Test </Text>
</View>
);
}
};
So it's worth noting that your setX methods may, or may not, complete in the sequence you have them in your code. Therefore, your booksRef call could be being made even though userBookTitles is an empty array. Which would explain why you're not getting any data on load.
You're setting userBookTitles in your first useEffect and the only other place I see you're using it is in your booksRef call. One easy fix would be to simple move booksRef inside the else statement of the first useEffect and simply pass it the userBookTitles there. This should help in solving your issue, if I understood it correctly.

How to pass an async object to a props in react?

I'm practicing react and how to incorporate react to firebase/firestore. Now I want to make something like a CMS using firestore database. I have successfully fetch data from the database and confirmed it by console log. But the problem is whenever I tried to pass the object to be used as my props it throws me an error. But if I add the code after the page is fully loaded, the data is successfully loaded and throws me an error after I refresh the page.
I think it has something to do with asynchronous request but I don't know how to pull it off.
const app = initializeApp(firebaseConfig)
const db = getFirestore(app);
const [data, setData] = useState({});
const fetchData =async () => {
const docRef = doc(db, "data", "RSVp8ljO95Dpwa0oSs0G");
const docSnap = await getDoc(docRef);
const dataTest = docSnap.data();
await setData(dataTest);
console.log("Document data:", dataTest);
}
useEffect(() => {
fetchData();
}, [])
return (
<div style={{overflow: 'hidden'}}>
<NavBar />
<div style={{width: '100%', height:'81vh', padding: '5%', overflow:'scroll'}}>
<Container >
<div className="d-flex flex-wrap justify-content-center">
<TrainingSchedule
**date={data.event1.date} //I want to pass the object here but throws me an error**
month='SEPT'
eventTitle='Lets Get to know each other'
eventDescription='Have a drink with our finest coach and enjoy the summer'
time='1pm'
backgroundColor= 'CadetBlue'
/>
......
Hi #Juls: Welcome to StackOverflow.
In your example, you're trying to access properties on data that don't exist in the first render (before the effect hook runs). That's why you're getting the error. Instead, check to make sure the object and properties that you need exist before trying to access them:
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.15.7/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useEffect, useState} = React;
async function getDataFromAPI () {
await new Promise(res => setTimeout(res, 1000));
return { event1: { date: '2021-09-18' } };
}
function Example () {
// Leave the state value undefined instead of initializing it as an empty object
const [data, setData] = useState();
useEffect(() => {
const fetchData = async () => {
const fetchedData = await getDataFromAPI();
setData(fetchedData);
};
fetchData();
}, []);
return (
<div>
{
// Check if the data exists before trying to access its properties:
data
? (<div>{data.event1.date}</div>)
: (<div>Data is being fetched...</div>)
}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
please provide us with the error so that we can fully understand the problem.
but for now, try this :
date={data.event1.date || null}
this should cause the date to become null if the data is not ready, then get updated in the next render which is caused by setState.

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?
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.

Call and API inside useEffect in react native?

I'm calling an Api to get data but the data is really heavy. I'm wondering if i'm calling it in right place inside useEffect or should i call it somewhere else. I've put the console.log to check but the number of console.log exceeded the number of objects i have in the API. My code is :
const ProductsList = () => {
const [products, setProducts] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
getProducts().then((response) => {
if (isMounted) {
console.log('im being called');
setProducts(response);
setLoading(false);
}
});
return () => { isMounted = false; };
}, [products]);
return (
<View style={styles.container}>
{isLoading ? <ActivityIndicator /> : ((products !== [])
&& (
<FlatList
data={products}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<Item
style={{ marginLeft: 35 }}
name={item.name}
date={item.date}
address={item.adress}
/>
);
}}
/>
)
)}
</View>
);
};
It looks like your effect goes round in a circle:
On each render the effect will look at products to see if it has changed.
If it has changed it will call your effect which fetches new products.
When you get new products you update your products state.
This causes the effect to run again.
You probably only want to run that effect once, when the component mounts. In which case you can simply write:
useEffect(() => {
getProducts().then((response) => {
setProducts(response);
setLoading(false);
});
}, []); // <-- note the empty array, which means it only runs once when mounted
If that single API call is too heavy, then you need to look more at things like pagination in your requests, or modifying the response to return only the data you really need. But that's outside the scope of this question, I think.
Let me know if you have any questions.

React inline Styling not applying

Hey guys I am just trying to render a set of paragraphs that contain news articles. I am using the inline styling method creating an object and assigning its value to the <h2> tag using the style attribute.
import React, {useState, useEffect } from 'react';
const App = () => {
//inLineStyles
const useStyle = {
backgroundColor: "#282c34",
color: "white",
padding: "40px",
fontFamily: "Arial",
textAlign: "center",
};
//State
const [news, setNews] = useState([]);
const [searchQuery, setSearchQuery] = useState("")
const [url, setUrl] = useState("http://hn.algolia.com/api/v1/search?query=Hacker")
const [loading, setLoading] = useState(false);
//APIcall
const fetchNews = () => {
setLoading(true);
fetch(url)
.then(responce => responce.json())
.then(data => (setNews(data.hits), setLoading(false)))
.catch(error => console.log(error))
}
useEffect(() => {
fetchNews();
}, [url])
const handleChange = (e) => {
setSearchQuery(e.target.value)
}
const handleSubmit = e => {
e.preventDefault()
setUrl(`http://hn.algolia.com/api/v1/search?query=${searchQuery}`)
}
const handleLoading = () => {
if (loading) {
return <h2> Loading...</h2>
}
else {
return
}
}
return (
<div>
<h1 style={useStyle}>News </h1>
{ handleLoading() }
<form onSubmit={handleSubmit}>
<input type='text' value={searchQuery} onChange={handleChange}/>
<button>Search</button>
</form>
{news.map((n, i) => (
<p key={i}> {n.title} </p>
))}
</div>
);
}
export default App;
Can you please let me know why the style is not applied?
Thanks!
I was stuck on this for half of the day. I finally was able to do figure it out. All I had to do was to refresh the page in the browser(I was saving and compiling the code in the code editor and for some reason it wasn't updating the web page). If someone might know why this happened please comment it on here.
Thanks for all the replies!

Categories