How to set a state Array from a global variable in React - javascript

I'm trying to set a state variable to the value of a global variable inside componentWillMount.
I'm making API calls based on the user's interests (using forEach function) and I`m trying to store the results in a global variable to latter store it in a state variable ( user:{articles}).
For some reason in the render the this.state.user.articles variable is always empty. Am I missing something ?
Here is how I set the initial value :
class Home extends Component {
constructor(props){
super(props);
this.state = {
user :{
articles: [],
}
}
this.componentWillMount.bind(this);
}
Here is where I make my API calls and try to use this.setState to update the varialbe
async componentWillMount(){
const loggedUser = await Auth.currentAuthenticatedUser();
const userEntry = await API.get(apiName,path + loggedUser.username);
console.log(userEntry)
currentInterests = userEntry.userInterests;
currentInterests.forEach(async function (interest) {
console.log(interest);
let query = 'q='+interest+'&';
let url = 'https://newsapi.org/v2/everything?' +
query +
'from=2019-02-22&' +
'sortBy=popularity&' +
'apiKey=hiddenforsecurity';
let req = new Request(url);
const response = await fetch(req);
const json = await response.json();
console.log(typeof json.articles);
for(var key in json.articles){
results.push(json.articles[key])
}
console.log(results[15]);
});
this.setState({
user : {
articles: results,
}
})
}
While console.log(results[15]) returns the expected element, in the render the console.log(this.state.user.articles) from
render() {
console.log(this.state.user.articles)
return (
<ul>
{this.state.user.articles.map((article, index) => {
console.log(article.author)
return (<li key={index}>{article.author}</li>)})}
</ul>
);
}
return an empty array, as the one set in the constructor which means that the function
this.setState({
user : {
articles: results,
}
})
from componentWillMount has no effect. What am I missing? I've tried countless fixes online and nothings seems to work.

The main issue is that forEach will not wait for each callback to have run. In the example below, done will be printed before the array elements (thing1, thing2, thing3).
const things = ["thing1", "thing2", "thing3"];
//https://gist.github.com/eteeselink/81314282c95cd692ea1d
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const exampleFunction = async() => {
things.forEach(async(thing) => {
await delay(500);
console.log(thing);
});
console.log("done");
}
exampleFunction();
In your example, the state will be set before the results have actually been processed.
One way this can be avoided is by using a for loop so that each statement can be awaited upon
const things = ["thing1", "thing2", "thing3"];
//https://gist.github.com/eteeselink/81314282c95cd692ea1d
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const exampleFunction = async() => {
for (let index = 0; index < things.length; index++) {
await delay(500);
console.log(things[index]);
};
console.log("done");
}
exampleFunction();

setState is called before the forEach is complete, here's a simple illustration :
const arr = [ 1, 2, 3,4,5];
arr.forEach(async e => {
const a = await fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
console.log(a)
})
console.log('after the loop')
Update your componentWillMount to use Promise.all like :
async componentWillMount(){
const loggedUser = await Auth.currentAuthenticatedUser();
const userEntry = await API.get(apiName,path + loggedUser.username);
currentInterests = userEntry.userInterests;
const promises = currentInterests.map(interest => {
let query = 'q='+interest+'&';
let url = 'https://newsapi.org/v2/everything?' +
query +
'from=2019-02-22&' +
'sortBy=popularity&' +
'apiKey=hiddenforsecurity';
let req = new Request(url);
return fetch(req);
})
const results = await Promise.all(promises)
.then(res => res.map(e => e.json()))
.then(res => res.map(res.articles));
this.setState({
user : {
articles: results,
}
})
}

Related

How to catch Firebase promise in React?

I have a simple function that checks if the user has Premium access or not:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.id === auth.currentUser.uid) {
return doc.data().userSettings.hasPremium
}
});
})
}
else{
return false
}
}
I tried to catch this in various ways, but no luck, it always returns an "undefined" object.
const getPremium = async => {
checkPremium.then((response) => console.log(response))
}
const getPremium = async => {
let hasPremium = await checkPremium()
}
let hasPremium = checkPremium()
What is the correct way to get the returned Boolean value?
onSnapshot is meant for listening to a collection continuously, getting repeatedly notified as its value changes. It does not create a promise, so the promise returned by getPremium is unrelated to the data you will eventually get in onSnapshot. If you just want to get the value once, you should use getDocs:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
const querySnapshot = await getDocs(q);
const match = querySnapshot.docs.find(doc => doc.id === auth.currentUser.uid);
if (match) {
return doc.data().userSettings.hasPremium);
} else {
return false;
}
}
else{
return false
}
}
Also, instead of getting all the users and then using client side code to find the one with the right id, you could just fetch that individual doc directly:
const ref = doc(db_firestore, 'users', auth.currentUser.uid)
const snapshot = await getDoc(ref);
const data = snapshot.data();
if (data) {
return data.userSettings.hasPremium
} else {
return false
}

Fetching data using asynchronous function call in javascript does not work (doesn't follow the execution sequence)

I am trying to build a React project where I have a component that fetches data from a remote server and plots it.
async function fetchRemoteData(name) {
let dataItem = null;
if (name) {
await fetch("/endpoint/" + name)
.then((res) => res.json())
.then((payload) => {
console.log("payload = " + JSON.stringify(payload));
dataItem = payload;
});
} else {
console.log("Error: No data for " + name);
}
return dataItem;
}
var RenderPlot = ({ match }) => {
let remoteObject = fetchRemoteData(match.params.data);
console.log("remoteObject = " + JSON.stringify(remoteObject));
// .... rendering code
// .... and other irrelevant stuffs to this question
}
If I run this code, on the console, I'm seeing the remoteObject is empty, but the payload inside the function fetchRemoteData() is not.
remoteObject = {}
payload = {"data":"some value"}
Now, let's say I have data stored in the browser (which is obviously I'm not supposed to do, but anyway) and if I fetch it using a generic function call, I'm not having problem.
function fetchLocalData(name) {
// read the data from a local js object
// and return it
}
var RenderPlot = ({ match }) => {
let remoteObject = fetchRemoteData(match.params.data);
console.log("remoteObject = " + JSON.stringify(remoteObject));
let localObject = fetchLocalData(match.params.data);
console.log("localObject = " + JSON.stringify(lovalObject.m))
// .... rendering code
// .... and other irrelevant stuffs to this question
}
and the output --
localObject = 4
remoteObject = {}
payload = {"data":"some value"}
So, what's happening is the code gets the localData from fetchLocalData(), then calls fetchRemoteData() but it doesn't wait if the data is received or not. And then, keeps doing whatever in the rest of the code.
What I want is, the code should wait until the remoteData is received and then proceed to the next steps.
How do I do that?
I can recommend you to store your received data in state of component like example below. If I understood you correctly -
const RenderPlot = ({ match }) => {
const [data, setData] = useState()
const fetchData = useCallback(async name => {
try {
const response = await fetch("/endpoint/" + name);
setData(response.data); // or whatever, depends on how you set your API up.
} catch (e) {
console.log(e)
}
}, [])
// in this case it works as componentDidMount;
useEffect(() => {
;(async function() {
await fetchData(match.params.data);
})()
}, [match.params])
return (<div>...your jsx plot implementation</div>)
}
According to this flow you can compose two fetching,
const fetch1 = useCallback(async () => {your first call api}, [])
const fetch2 = useCallback(async () => {your second call api}, [])
useEffect(() => {
;(async function() {
await fetch1...
await fetch2
})()
}, [match.params])
Or it can be like this
const fetchData = useCallback(name => {
try {
const response1 = await fetch("/endpoint1/" + name);
const response2 = await fetch("/endpoint2/" + name);
// this line would be read after both of your responses come up. Here you can make some operation with both data.
setData(response.data); // or whatever, depends on how you set your API up.
} catch (e) {
console.log(e)
}
}, [])
The reason is that ...return dataItem executes before that promise resolve the request. You need to use async await:
async function fetchRemoteData(name) {
let dataItem = null;
if (name) {
const res = await fetch("/endpoint/" + name);
dataItem = await res.json();
} else {
console.log("Error: No data for " + name);
}
return dataItem;
}

Promise inside a loop inside an async function

I am working on a project using react and firebase and redux and I have some items that did created by a user. I'm storing the id of the user in the item object so i can populate the user later when i get the item to display.
Now I'm trying to get the items and modify them by replacing the user id with the actual info about the user but I have a promises problem. In my code I just get an empty array which mean the modification didn't get resolved before I return the final result.
export const getItems = () => {
return (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const items = [];
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
return dbRef
.get()
.then((res) => {
const firstVisible = res.docs[0];
const lastVisible = res.docs[res.docs.length - 1];
async function getData(res) {
/////////////////////////////////////////////// how to finish this code befor jumping to the return line
await res.forEach((doc) => {
firestore
.collection('users')
.doc(doc.data().owner)
.get()
.then((res) => {
items.push({ ...doc.data(), owner: res.data() });
});
});
////////////////////////////////////////////////
return { docs: items, lastVisible, firstVisible };
}
return getData(res);
})
.catch((err) => {
console.log(err);
});
};
};
I don't get exactly what you are trying to do, but I would suggest putting some order to make your code easy to read and work with.
You can use for of to manage async looping. I suggest something like this, disclaimer, I did it at the eye, problably there are some errors, but you can get the idea.
const getAllDocs = function (data) {
let temp = [];
data.forEach(function (doc) {
temp.push(doc.data());
});
return { data: temp };
};
const getDoc = snap => (snap.exists ? { data: snap.data() } : {});
export const getItems = () => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items } = await dbRef.get().then(getAllDocs);
const firstVisible = items[0];
const lastVisible = items[items.length - 1];
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
const owner = {
/* whatever this means*/
};
temps.push({ ...user, owner });
}
return { docs: temps, lastVisible, firstVisible };
};
};
The problem is that an array of Promises is not itself a Promise -- so awaiting it will be a no-op.
You can solve this using Promise.all if you want to load them all asynchronously.
const items = await Promise.all(res.map(async (doc) => {
const res = await firestore.collection('users').doc(doc.data().owner).get();
return { ...doc.data(), owner: res.data() };
});
Otherwise you can await in a for loop as suggested in other answers.

firestore get data from two tables one after the other return empty array

I'm using firebase - firestore. I have courses and tasks collection.
I want to get all the courses of user from courses collection and for each course to get days data from tasks collection and then save all this data in one array.
getData = () => {
var arr = []
f.auth().onAuthStateChanged(async (user) => {
db.collection("courses")
.where("uid", "==", user.uid)
.get()
.then((snapshot) => {
var a = {};
snapshot.forEach((doc) => {
let coursesData = doc.data()
let courseName = coursesData.name;
let kita = coursesData.kita;
a = { name: courseName, id: doc.data().code, k: kita };
let snapshotData = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get();
let numActiveCourse = 0;
snapshotData.forEach((dc) => {
let taskData = dc.data()
console.log('taskData',taskData)
let days = taskData.days;
if (days > 0) {
numActiveCourse = 1;
}
});
a = { ...a, numActiveCourse };
arr.push(a);
console.log("arr2 is", arr);
});
})
.catch((e) => {
console.log("error is courses", e);
});
this.setState({data:arr})
});
};
the problem is that the arr is always empty (I guess I have asyncornize issue)
and the snapshot not await after it will finish.
I found solution.
the issue it's because I tried to make async await into forEach and it not wait to answer.
the solution is
readCourses = async()=>{
f.auth().onAuthStateChanged(async (user) => {
let loadedPosts = {};
let docSnaps = await db.collection("courses").where("uid", "==", user.uid).get();
for (let doc of docSnaps.docs){
let courseName = doc.data().name;
let kita = doc.data().kita
loadedPosts[doc.id] = {
...doc.data(),
k:kita,
id:doc.data().code
}
const taskSnap = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get()
let numActiveCourse = 0
for(let task of taskSnap.docs){
let taskData = task.data()
if(taskData.days>0){
numActiveCourse =numActiveCourse+1
}
}
loadedPosts[doc.id].numActiveCourse = numActiveCourse
}
console.log('loadedPosts',loadedPosts)
this.setState({data:loadedPosts})
})
}
if you have any other solution I would like to see.
It's not a good idea to mix await and then as it was in your original code. There was your first mistake. Not only you didn't wait for results of forEach, but this.setState({data:arr}) was outside of then and executed even before you've reached the forEach call.
Another issue with your initial version of code is as you said - not waiting for results of forEach. But I'm not sure that you fully understand it. Because you didn't have to change your code so much (readability aside). All your had to do is:
// change db.collection("courses")...then(...) to
const snapshot = await db.collection("courses")... // only now onAuthStateChanged callback becomes async
...
// then change forEach() to map() and wait for result
const promises = snapshot.map(async (doc) => { ... })
await Promise.all(promises)
...
As for your new version: in each iteration of the for loop you await. That means that requests for every taskSnap will be executed one after another. That's bad. Especially on slow connections. Check out the snippet (I've simplified it to the bare minimum): getData with map completes in 1 second, version with for - in 4 seconds. (And you also removed catch from your new code - not a great idea.)
let i = 0
const get_courses = () => new Promise((resolve) => setTimeout(() => resolve(["a","b","c","d"]), 10))
const get_tasks = () => new Promise((resolve) => setTimeout(() => resolve(++i), 1000))
const f_auth_onAuthStateChanged = fn => fn()
const getData = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
const promises = courses.map(async (course) => {
const tasks = await get_tasks()
data.push({ course, tasks })
})
await Promise.all(promises)
console.log(data) // this.setState({ data })
console.timeEnd("map")
} catch(e) { console.error(e) }
})
}
console.time("map")
getData()
const getData2 = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
for (const course of courses) {
const tasks = await get_tasks()
data.push({ course, tasks })
}
console.log(data) // this.setState({ data })
console.timeEnd("for")
} catch(e) { console.error(e) }
})
}
console.time("for")
getData2()
The readCourses function from your own answer doesn't return a Promise. So formally it's not async. That won't change anything, except for a small code readability improvement. Same goes for onAuthStateChanged callback from your original code.

Making Firebase query act synchronous

I'm building a simple testing site. How it works: A user accepts a task (assignment), sends an answer, which is then pushed to an array called answers.
What I need to do, is check if the answers array in the assignment object is defined. If it is it means user has submitted at least one answer and he's good to submit.
The problem is the async. I'm not sure how to make the code wait for the query to finish.
Here's my code:
export default class FinishAnswerButton extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleFinishAssignment = this.handleFinishAssignment.bind(this);
}
async handleFinishAssignment() {
var currentAssignment = firebase.database().ref('Works').child(this.props.assignmentId);
console.log('props points', this.props.points);
var outerThis = this;
await currentAssignment.on('value', snapshot => {
console.log(snapshot.val());
});
var user = firebase.database().ref('Users').child(firebase.auth().currentUser.uid);
await user.once('value', snapshot => {
var acc = snapshot.val();
var points = outerThis.props.points + acc.points;
user.child('points').set(points).catch(e => console.log(e));
// user.child('assignedWork').remove().catch(e => console.log(e));
// currentAssignment.child('state').set('Completed').catch(e => console.log(e));
// currentAssignment.child('finishTime').set(Time.generate()).catch(e => console.log('finishTime', e));
});
return <AssignmentsComponent/>
}
render() {
return (
<Button onClick={this.handleFinishAssignment}>Zakoncz rozwiazywanie zadania (Upewnij
sie ze uczen ma wszystko czego potrzebuje!)</Button>
)
}
}
I tried solving it like this:
async handleFinishAssignment() {
var currentAssignment = firebase.database().ref('Works').child(this.props.assignmentId);
console.log('props points', this.props.points);
var outerThis = this;
currentAssignment.once('value', snapshot => {
console.log(snapshot.val());
return snapshot.val();
}).then(assignment => { // here
console.log(assignment);
var user = firebase.database().ref('Users').child(firebase.auth().currentUser.uid);
if (assignment.answers.length > 0) {
console.log('if');
user.once('value', snapshot => {
var acc = snapshot.val();
var points = outerThis.props.points + acc.points;
user.child('points').set(points).catch(e => console.log(e));
// user.child('assignedWork').remove().catch(e => console.log(e));
// currentAssignment.child('state').set('Completed').catch(e => console.log(e));
// currentAssignment.child('finishTime').set(Time.generate()).catch(e => console.log('finishTime', e));
}).catch(e => console.log(e));
} else
console.log('else');
});
return <AssignmentsComponent/>
}
But it turned out, the assignment passed to the callback is a promise, not the actual snapshot.val(), which is basically what I need in the next part of the code.
Also, why does adding await not solve the issue? I thought the whole idea of await is to make the code act synchronously. Or am I using it wrong?
So to recap: I need to make this code wait. I need to use the response from the await currentAssignment.on('value', snapshot => { to be available in the next query, which is: await user.once('value', snapshot => {. How can I achieve this?
Ok I figured it out. I was on the right track, but got a mental hiccup somewhere on the way. Here's the working code:
async handleFinishAssignment() {
var currentAssignment = firebase.database().ref('Works').child(this.props.assignmentId);
var outerThis = this;
currentAssignment.once('value').then(snapshot => { // Right here
var assignment = snapshot.val();
console.log(assignment);
var user = firebase.database().ref('Users').child(firebase.auth().currentUser.uid);
if (assignment.answers !== undefined) {
console.log('if');
user.once('value', snapshot => {
var acc = snapshot.val();
var points = outerThis.props.points + acc.points;
user.child('points').set(points).catch(e => console.log(e));
// user.child('assignedWork').remove().catch(e => console.log(e));
// currentAssignment.child('state').set('Completed').catch(e => console.log(e));
// currentAssignment.child('finishTime').set(Time.generate()).catch(e => console.log('finishTime', e));
}).catch(e => console.log(e));
} else
console.log('else');
});
return <AssignmentsComponent/>
}

Categories

Resources