React Hooks, setState not working with an async .map call - javascript

So, I'm trying to change a state in my component by getting a list of users to call an api.get to get data from these users and add on an new array with the following code:
function MembersList(props) {
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => {
return dev.login;
});
handleDevs(arr);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [membersList]);
function handleDevs(membersArr) {
membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
setDevs([...devs, data]);
});
}
but the devs state always return an empty array, what can I do to get it to have the actual users data on it?

The issue you were having is because you were setting devs based on the data from the original render every time due to the closure around handleDevs function. I believe this should help take care of the issues you were having by using the callback method of using setDevs.
This also takes care of some issues with the dependency arrays and staleness in the useEffect hook. Typically using // eslint-disable-next-line react-hooks/exhaustive-deps should be your last resort.
function MembersList(props) {
// this isn't needed unless you are using it separately
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => dev.login);
arr.forEach(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
setDevs((devs) => [...devs, data]);
})
}, [membersList]);
}

You need to understand how React works behind scenes.
In short, it saves all the "sets" until it finishes the cycle and just after that actually update each state.
I think that why you do not see the current state updated.
For better understanding read this post: Medium article

The issue is that setDevs uses devs which is the version of devs when handleDevs is defined. Therefore setDevs will really only incorporate the data from the last time setDevs is called.
To fix this you can use the callback version of setDevs, like so:
setDevs(prevDevs => [...prevDevs, data])
Also since you are not trying to create a new array, using map is not semantically the best loop choice. Consider using a regular for loop or a forEach loop instead.

you call setDevs in the async execution loop. Here is the updated handleDevs function.
function handleDevs(membersArr) {
const arr = membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
return data;
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
});
setDevs([...devs, ...arr]);
}

Related

setState in useEffect not update

Got an input and want to pass value to handler:
const [term, setTerm] = useState('');
<Input type="text" onBlur={(e)=>handleFilter(e, 'params')} />
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}
I want to make an search filter function, for ex. if I enter a, console return empty, then I enter b console return a ! it means term not update immediately, then I used useEffect but inside the useEffect I got new value, but inside handleFilter function still console return prev value.
useEffect(() => {
getApi()
.then(data => {
console.log(data)
})
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])
I tried this but no success:
setTerm({...term, e.target.value});
Any solution? I'm new to react hook.
You can check this answer here.
This is because react's state update is async. You can't rely on its update right after calling setState. Put your effects (code that is run after a state is updated) in a useEffect hook.
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
}
React.useEffect(() => {
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}, [term]);
setTerm is async and will update the term on the next render cycle.
it is not updated immediately for the current render cycle.
you can store the current value in a ref if you are curious what is happening behind the scenes
const termRef = React.useRef(term);
termRef.current = term;
const yourHandler = () => {
setTimeout(() => console.log(termRef.current), 0);
}
If we are going ahead with useEffect with an API call, please ensure to include async await scenario to ensure that setState updates after the data is fetched.
useEffect(async () => {
const data = await getApi()
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])
I could be wrong on this one, but could this have something to do with you calling an async function from a normal event. This might cause some type of delay.
There is also the fast that useEffect is treated differently in React than a normal function, since it's integrated into React.
It could also be related to the [term] trigger in the useEffect but the event in your handleFilter isn't treated the same

js only - run a function only once

I'm fetching some data from firebase and would like to run async/await function (to fetch data) only once upon the first page load. I'm used to React and lifecycle methods / hooks doing it but this little project is just too small to use React. I just need to run this function once, fetch the data, save it to a variable and do not make any further calls to firebase api in the same session.
async function getEntries() {
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
}
Is there any js-only way of running this function only once when the page loads?
If you call a function just once, why do you need the function at all?
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
This top level await only works in modules, and it blocks all depending modules to load. If that is not necessary (they don't depend on the data), or if you don't want write a module, you can wrap the code in an async IIFE, and store the returned promise in a variable:
const dataPromise = (async function() {
//...
return data;
})();
While the data is loading, you might want to show some loading icon or so. That can easily be done with the following hook:
function usePromise(p) {
const [state, setState] = useState(null);
useEffect(() => { p.then(setState); }, []);
return state;
}
// Inside a component:
const data = usePromise(dataPromise);
if(data === null)
return <Loading />;
// show data
Yes. You can use Self Invoking (self executing) Functions. Syntax is like:
(function(){})();
The last parentheses are for running function. the function is anonymous.
You can Implement it this way:
(async function () {
const snapshot = await firebase.firestore().collection('riders').get()
})();
in this way you can never call this function again and it will run only once.
Tutorial: https://blog.mgechev.com/2012/08/29/self-invoking-functions-in-javascript-or-immediately-invoked-function-expression/
And The question you asked is somehow duplicate and answered here: Function in JavaScript that can be called only once
What you are looking for is memoization of the function result. There are several libraries to supporting including react.
Theres also a handmade pattern you can use by changing the function implementation after it's called once, accoring to JavaScript: The Good Parts
async function getEntries() {
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
getEntries = async function(){
return snapshot
}
return snapshot
}
I think you can load it with the load method when the page is first loaded and then set it to cookie or local stroge. You can check this value on next page loads. You can do this quickly using jQuery.
$(window).load(function() {
var item = localStorage.getItem('test');
if(item != null){
// your code
}
else {
localStorage.setItem('test', 1);
}
});
The simplest way is to make a global variable like:
let isCalled = false;
and in the function body do:
if(isCalled) return;
//the stuff the function would do
isCalled = true;
//Assign isCalled to true before using a return statement as it will make the program discard the lines below it.

How to iterate through a Firestore snapshot documents while awaiting

I have been trying to obtain a series of documents from firestore, reading them and acting accordingly depending on a series of fields. The key part is I want to wait for a certain process while working on each document. The official documentation presents this solution:
const docs = await firestore.collection(...).where(...).where(...).get()
docs.forEach(await (doc) => {
//something
})
The problem with this solution is taht when you have a promise inside the forEach it won't await it before continuing, which I need it to. I have tried using a for loop:
const docs = await firestore.collection(...).where(...).where(...).get()
for(var doc of docs.docs()) {
//something
}
When using this code Firebase alerts that 'docs.docs(...) is not a function or its return value is not iterable'. Any ideas on how to work around this?
Note that your docs variable is a QuerySnapshot type object. It has an array property called docs that you can iterate like a normal array. It will be easier to understand if you rename the variable like this:
const querySnapshot = await firestore.collection(...).where(...).where(...).get()
for (const documentSnapshot of querySnapshot.docs) {
const data = documentSnapshot.data()
// ... work with fields of data here
// also use await here since you are still in scope of an async function
}
I have found this solution.
const docs = [];
firestore.collection(...).where(...).get()
.then((querySnapshot) => {
querySnapshot.docs.forEach((doc) => docs.push(doc.data()))
})
.then(() => {
docs.forEach((doc) => {
// do something with the docs
})
})
As you can see this code stores the data in an extern array and only after this action it works with that data
I hope this helped you to solve the problem!

A cleaner way to query a collection and return the data as a list?

It seems like there is a cleaner and more optimised way to query a Firestore collection, call doc.data() on each doc, and then return an array as result. The order in which the docs are pushed into the result array feels haphazard.
There are many steps to this code:
A new result variable is created
A query is made to retrieve the 'stories' collection
For each doc, we call the doc.data()
We push each doc to the result array
Return the result array
function getStories() {
var result = [];
db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
})
return result;
}
The code works fine but it seems like we can write this code in a cleaner way with less steps.
The code you shared actually doesn't work, as George comment. Since Firestore loads data asynchronously, you're always returns the array before the data has been loaded. So your array will be empty.
In code:
function getStories() {
var result = [];
db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
})
return result;
}
var result = getStories();
console.log(result.length);
Will log:
0
To fix this, you want to return a promise, which resolves once the data has loaded. Something along these lines:
function getStories() {
var result = [];
return db.collection('stories').get().then(querySnapshot => {
querySnapshot.forEach(doc => result.push(doc.data()));
return result;
})
}
So this basically added two return statements, which makes your result bubble up and then be returned as the promise from getStories. To invoke this, you'd do:
getStories().then(result => {
console.log(result.length);
})
Which will then log the correct number of results.
First, define a map function for use with firebase (named mapSnapshot because it is meant specifically for use with firebase):
const mapSnapshot = f => snapshot => {
const r = [];
snapshot.forEach(x => { r.push(f(x)); });
return r;
}
Then, you can just work with Promise and mapSnapshot:
function getStories() {
return db.collection('stories').
get().
then(mapSnapshot(doc => doc.data()));
}
Usage example:
getStories().then(docs => ... do whatever with docs ...)
To be honest, it isn't less code if used as a one-time solution. But the neat thing here is, it allows to create reusable abstractions. So for example, you could use mapSnapshot to create a snapshotToArray function which can be reused whenever you need to convert the DataSnapshot from firebase to a normal Array:
const snapshotToArray = mapSnapshot(x => x);
Please note: This doesn't depend on any collection name whatsoever. You can use it with any DataSnapshot from any collection to convert it into an Array!
That's not all. You can just as easily create a function from the contents of a DataSnapshot into a regular Array with the contents in it:
const readDocsData = mapSnapshot(x => x.data());
Again, this doesn't look like a big deal – until you realize, you can also create a fromFirebase function, which allows to query various datasets:
const fromFirebase = (name, transform = x => x) => {
return db.collection(name).get().then(transform);
}
And which you can then use like this:
fromFirebase('stories', readDocsData).then(docs => {
// do what you want to do with docs
});
This way, the final result has less steps you as a programmer notice immediatly. But it produced (although reusable) several intermediate steps, each hiding a bit of abstraction.

Axios API call returning Promise object instead of result?

Before I start, let me say that I'm new to Javascript and very new to axios API calls, so I'm probably making a rookie mistake...
I have this function getObjects() that's meant to map over an array and return the data from an Axios API call. The API call and map function are both working, but I'm getting a Promise object instead of the data I'm trying to get.
I figure this is because the data is returned before there's enough time to actually get it, but not sure how to fix? I tried a .setTimeout(), but that didn't seem to work.
getObjects() {
let newsItems = this.state.arrayofids.map((index) => {
let resultOfIndex = axios.get(`https:\/\/hacker-news.firebaseio.com/v0/item/${index}.json`).then((res) => {
let data = res.data;
//console.log(data.by); // this prints the correct byline, but
// all the bylines are printed after
// the console.log below...
if (!data.hasOwnProperty('text')) return data;
}); /// end of axios request
return resultOfIndex;
}); /// end of map
/// ideally this would end in an array of response objects but instead i'm getting an array of promises...
console.log(newsItems);
}
(The extra escape characters are for my text editor's benefit.)
Here's a link to a codepen with the issue - open up the console to see the problem. It's a React project but I don't think any of the React stuff is the issue. EDIT: Codepen is link to working solution using axios.all as suggested below
Thanks!
EDIT: Here is my working solution.
getObjects() {
let axiosArr = [];
axios.all(this.state.arrayofids.map((id) => {
return axios.get(`https:\/\/hacker-news.firebaseio.com/v0/item/${id}.json`)
})).then((res) => {
for (let i = 0; i < this.state.arrayofids.length; i++) {
axiosArr.push(<li key={i} data-url={res[i].data.url} onClick={(e) => this.displayTheNews(e)}>{res[i].data.title}</li>);
}
if (axiosArr.length == this.state.arrayofids.length) {
this.setState({arrayofdata: axiosArr});
console.log('state is set!');
}
})
}
axios.all function should be more appropriate to your current scenario.
Your console.log is executing immediately, rather than waiting for the requests to finish, because they are not synchronous. You have to wait for all the responses before you console.log.
OPTION 1 (the hard way):
replace your console.log with
newsItems.forEach((promise, index) => {
promise.then((object)=>{
newsItems[index] = object
if (index+1 == newsItems.length) {
console.log(newsItems)
}
})
})
OPTION 2 (the better way):
using axios.all
getObjects() {
axios.all(this.state.arrayofids.map((id) => {
return axios.get(`https:\/\/hacker-news.firebaseio.com/v0/item/${id}.json`)
})).then((res) => {
console.log(res)
})
}
by the way, I would definitely reccommend changing
this.state.arrayofids.map((index) => {
let resultOfIndex = axios.get(`https:\/\/hacker-news.firebaseio.com/v0/item/${index}.json`)...
to be called id instead of index

Categories