javascript async await with react. setState doesn't wait forawait - reactjs

I am building a component tat get the members of a group and then download the image from the api. It uses async await in the componentDidMount and the photos come back correctly. however even though I am using async await. it doesn't wait for the images to resolve. Thus in setState it will return a promise.
my component:
import React from "react";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faUser } from "#fortawesome/free-solid-svg-icons";
import {
GetGroupMembers,
getImage
} from "../../../HttpRepositories/oneApiRepository";
class TeamCardPersonaMembers extends React.Component {
constructor(props) {
super(props);
this._isMounted = false;
}
state = {
members: []
};
async componentDidMount() {
this._isMounted = true;
let members = await GetGroupMembers(this.props.teamId);
const membersToSend = await members.map(async member => {
const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
member.id
}/photo`;
let memberPhoto = await getImage(memberPhotoUrl);
member.photo = memberPhoto;
return member;
});
if (this.state.member !== "error" && this._isMounted) {
this.setState({ members: membersToSend });
}
}
render() {
let members = [];
console.log(this.state);
if (this.state.members.length > 0) {
this.state.members.map(member => {
console.log("this is the member ", member);
const memberImg = (
<img
key={member.id}
src={member.photo}
alt={member.displayName}
className="team-card-persona-wrapper"
title={member.displayName}
/>
);
members.push(memberImg);
});
}
return <React.Fragment>{members}</React.Fragment>;
}
componentWillUnmount() {
this._isMounted = false;
}
}
console.log
export default TeamCardPersonaMembers;
any help or pointers will be greatly appreciated! Cheers!

The standard implementation of map does not await the callback passed to it. Resulting in the map call returning promises before the callbacks complete. You have two choices.
const membersToSend = []
for (const member of members) {
const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
member.id
}/photo`;
let memberPhoto = await getImage(memberPhotoUrl);
member.photo = memberPhoto;
membersToSend.push(member);
}
or to roll your own implementation of async map.
Array.prototype.mapAsync = async function (callback) {
const output = []
for (const el of this)
output.push( await callback(el) ) <-- // Note the await on the callback
return output
}
Edit:
Note that the above method executes the promises in sequence. As Jed points out, if you want to fire all the requests at once and wait for them to resolve,
const memberPhotos = await Promise.all(members.map(member => {
const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
member.id
}/photo`;
return getImage(memberPhotoUrl);
}))
But this method could overload your network if there are too many requests, use with caution.

The await keyword only works on other async functions, or functions that return a Promise. So I don't think it makes sense to await on members.map like you are. That is just a normal sync function call. You need to set it up more like:
https://flaviocopes.com/javascript-async-await-array-map/

Related

handling event and interchangeable state in hooks

I have this small web app which I wrote when I was studying react, of course its in Class based components
As I learnt more I've decided to move over to hooks but I can't get my mind around it, imo I think class based components was more straightforward
So in Class based components I had my configiration as follows
State:
this.state = { countryData: {updated:0,cases:0, todayCases:0, deaths:0, todayDeaths:0, recovered:0, active:0}}
Setting initial state:
async getData(){
const resApi = await Axios.get('https://corona.lmao.ninja/v2/all')
this.setState({ countryData : resApi.data })
}
componentDidMount(){ this.getData() }
And then I have a dropdown menu of options which on change changes the country data
async getCountryData(event){
if (!event) return this.getData()
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`)
this.setState({ countryData : res.data })
}
So now I've tried to make the same thing in hooks I've started with
const [countryData, setcountryData] = useState({updated:0,cases:0, todayCases:0, deaths:0, todayDeaths:0, recovered:0, active:0})
Then for the previous 2 methods
const useChoosenCountry = (event) => {
useEffect(() => {
async function getdata(event){
if (!event) {
const resApi = await Axios.get('https://corona.lmao.ninja/v2/all')
setcountryData(resApi.data)
}
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`);
setcountryData(res.data);
}
console.log(event)
getdata()
},[event])
}
But it looks like I'm really far it's not console logging the event on change
componentDidMount can be replaced with useEffect(function, []):
useEffect(() => {
getData();
}, [])
const getData = async () => {
const res = await Axios.get('https://corona.lmao.ninja/v2/all')
setcountryData(res.data)
}
const getCountryData = async (event) => {
if (!event) return this.getData()
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`)
setcountryData(res.data)
}

How to setState after getting data from Firestore

I am currently able to get user data from the Firestore however I'm having trouble saving the users document data. I'm getting an error below in my console
TypeError: this.setState is not a function
at Object.next (RepRequest.js:32)
at index.cjs.js:1344
at index.cjs.js:1464
I attempted to follow another user's question from
Can't setState Firestore data, however still no success.
I do have a two api request right after getting the data and I am able to setState then. I tried incorporating the Firestore request in the promise.all but was unable to successfully, which is why I have it separated. Maybe I'm headed down the wrong path, any guidance is appreciated.
import React, { useEffect, useState } from "react";
import app from "./config/base.js";
import axios from "axios";
export default class RepRequest extends React.Component {
constructor(props) {
super(props);
this.state = {
userInfo: [],
fedSens: [],
fedReps: []
};
}
componentDidMount() {
const items = [];
app.auth().onAuthStateChanged(function(user) {
if (user) {
console.log("User is signed in");
let db = app
.firestore()
.collection("user")
.doc(user.uid);
db.get().then(doc => {
if (doc.exists) {
console.log("Document data:", doc.data());
items.push(doc.data());
} else {
console.log("No doc exists");
}
});
}
this.setState({ userInfo: items });
});
Promise.all([
axios.get(
`https://api.propublica.org/congress/v1/116/senate/members.json`,
{
headers: { "X-API-Key": "9wGKmWl3kNiiSqesJf74uGl0PtStbcP2mEzSvjxv" }
}
),
axios.get(
`https://api.propublica.org/congress/v1/116/house/members.json`,
{
headers: { "X-API-Key": "9wGKmWl3kNiiSqesJf74uGl0PtStbcP2mEzSvjxv" }
}
)
]).then(([rest1, rest2]) => {
this.setState({
fedSens: rest1,
fedReps: rest2
});
});
}
render() {
if (this.state.fedReps.length <= 0)
return (
<div>
<span>Loading...</span>
</div>
);
else {
console.log(this.state.fedReps);
return <div>test</div>;
}
}
}
Your problem arises from mixing lambda function declarations ((...) => { ... }) and traditional function declarations (function (...) { }).
A lambda function will inherit this from where it was defined but a traditional function's this will be isolated from the context of where it was defined. This is why it is common to see var self = this; in legacy-compatible code because this usually didn't match what you wanted it to.
Here is an example snippet demonstrating this behaviour:
function doSomething() {
var anon = function () {
console.log(this); // 'this' is independent of doSomething()
}
var lambda = () => {
console.log(this); // inherits doSomething's 'this'
}
lambda(); // logs the string "hello"
anon(); // logs the 'window' object
}
doSomething.call('hello')
Solution
So you have two approaches available. Use whichever you are comfortable with.
Option 1: Use a lambda expression
app.auth().onAuthStateChanged(function(user) {
to
app.auth().onAuthStateChanged((user) => {
Option 2: Assign a "self" variable
const items = [];
app.auth().onAuthStateChanged(function(user) {
// ...
this.setState({ userInfo: items });
}
to
const items = [];
const component = this; // using "component" makes more sense than "self" in this context
app.auth().onAuthStateChanged(function(user) {
// ...
component.setState({ userInfo: items });
}

MobX don't update react DOM in fetch promise callback

I am trying to update a react dom by changing an observable mobx variable inside a fetch callback in a react typescript app but mobx don't show any reaction on variable change.
I define my variable like this:
#observable data:any = []
and in my constructor i change data value:
constructor(){
this.data.push(
{
count:0,
dateTime:'2017'
})
this.getData();
}
its work fine and update dom properly as expected.
in getData() method i write a fetch to retrive data from server :
#action getData(){
this.data.push(
{
count:1,
dateTime:'2018'
})
fetch(request).then(response=>response.json())
.then(action((data:Array<Object>)=>{
this.data.push(data)
console.log(data)
}));
}
so my view now shows 2 value the 2017 and 2018 object data but the 2019 data that I get from the server is not showing. the log shows the correct values and variable filled in a right way but mobx don't update view after I set any variable in fetch function callback and I don't know why?
p.s: I do the same in ECMA and there was no problem but in typescript mobx act differently
Check my approach:
import { action, observable, runInAction } from 'mobx'
class DataStore {
#observable data = null
#observable error = false
#observable fetchInterval = null
#observable loading = false
//*Make request to API
#action.bound
fetchInitData() {
const response = fetch('https://poloniex.com/public?command=returnTicker')
return response
}
//*Parse data from API
#action.bound
jsonData(data) {
const res = data.json()
return res
}
//*Get objects key and push it to every object
#action.bound
mapObjects(obj) {
const res = Object.keys(obj).map(key => {
let newData = obj[key]
newData.key = key
return newData
})
return res
}
//*Main bound function that wrap all fetch flow function
#action.bound
async fetchData() {
try {
runInAction(() => {
this.error = false
this.loading = true
})
const response = await this.fetchInitData()
const json = await this.jsonData(response)
const map = await this.mapObjects(json)
const run = await runInAction(() => {
this.loading = false
this.data = map
})
} catch (err) {
console.log(err)
runInAction(() => {
this.loading = false
this.error = err
})
}
}
//*Call reset of MobX state
#action.bound
resetState() {
runInAction(() => {
this.data = null
this.fetchInterval = null
this.error = false
this.loading = true
})
}
//*Call main fetch function with repeat every 5 seconds
//*when the component is mounting
#action.bound
initInterval() {
if (!this.fetchInterval) {
this.fetchData()
this.fetchInterval = setInterval(() => this.fetchData(), 5000)
}
}
//*Call reset time interval & state
//*when the component is unmounting
#action.bound
resetInterval() {
if (this.fetchInterval) {
clearTimeout(this.fetchInterval)
this.resetState()
}
}
}
const store = new DataStore()
export default store
as #mweststrate mentioned in the comments, it was an observer problem and when I add #observer on top of my react class the problem get fixed

React native not waiting for response from API before continuing

I have just started playing about with react native and I have a problem that functions aren't waiting for responses before continuing.
So in Chrome my console log displays:
userStore
this state contents
returned data from api / userstore [object Object]
Basically getUserDetails is executed and in that time while the api is being called the setData function runs, and it completes before the api result has been returned.
I would like the getUserDetails functio to complete before setData is called.
I have had a look at resources online, but am at a loss. The code I am using is below (This has been stripped down for ease of reading nb. I am using mobx)
UserScreen.js
constructor (props) {
super(props);
this.state = {
data: null
};
}
async componentDidMount() {
this.props.commonStore.setLoading(true);
await this.props.userStore.getUserDetails('1');
this.setData();
this.props.commonStore.setLoading(false);
}
setData() {
this.setState({
userDetails: this.props.userStore.userDetails
});
console.log('userStore' + this.props.userStore.userDetails)
console.log('this state contents '+ this.state.userDetails);
}
render () {
if(this.props.commonStore.isLoading===false) {
return (<View><Text>Ready!!</Text></View>)
}else{}
return (<View><Text>Loading</Text></View>)
}
}
UserStore.js
#action getUserDetails = (userID) => {
axios.get('http://192.168.1.9/user/' + userID)
.then(response => {
console.log('returned data from api / userstore ' +response.data.user);
this.userdetails = response.data.user;
}).catch(error => {
console.log(error);
this.error = error
}) }
Thanks
If you have stumbled upon the beauty of Mobx, you need to move towards a stateless solution i.e.:
UserScreen.js
componentDidMount() {
this.getUserDetails();
}
async getUserDetails(){
await this.props.UserStore.getUserDetails('1');
}
render () {
const { isLoading, userDetails, error } = this.props.UserStore
return (<View>
{(!!isLoading)?<Text>{userDetails}</Text>:<Text>Loading</Text>}
</View>)
}
UserStore.js
#observable userdetails = {};
#observable isLoading = false;
#observable error = {};
async getUserDetails(userID) {
this.isLoading = true;
try {
await axios.get('http://192.168.1.9/user/' + userID)
.then(response => {
console.log('returned data from api / userstore '+response.data.user);
this.userdetails = response.data.user;
this.isLoading = false;
})
.catch(error => {
console.log(error);
this.error = error
})
} catch (e) {
console.log('ERROR', e);
this.isLoading = false;
}
}
As you are passing the data into an observable array i.e. #observable userdetails = {}; Mobx will automatically update the state, once the promise / await is complete.
P.S. Mobx rules OK!! :o)

AsyncStorage.getItem in react native not working as expected

I am trying to fetch data using AsyncStorage. whenever i call my action creator requestData and do console on the data which is passed , i get something like below .I have two version of getItem .In both the version i get useless value for property field . Property value should be readable
{"fromDate":"20160601","toDate":"20160701","property":{"_40":0,"_65":0,"_55":null,"_72":null},"url":"/abc/abc/xyz"}
async getItem(item) {
let response = await AsyncStorage.getItem(item);
let responseJson = await JSON.stringify(response);
return responseJson;
}
async getItem(item) {
try {
const value = AsyncStorage.getItem(item).then((value) => { console.log("inside componentWillMount method call and value is "+value);
this.setState({'assetIdList': value});
}).then(res => {
return res;
});
console.log("----------------------------value--------------------------------------"+value);
return value;
} catch (error) {
// Handle errors here
console.log("error is "+error);
}
}
componentWillMount() {
requestData({
fromDate: '20160601',
toDate: '20160701',
assetId: this.getItem(cmn.settings.property),
url: '/abc/abc/xyz'
});
}
You are getting property as a promise, you need to resolve it.
Try to use something link that.
assetId: this.getItem(cmn.settings.property).then((res) => res)
.catch((error) => null);
Since AsyncStorage is asynchronous in nature you'll have to wait for it to return the object AND THEN call your requestData method; something like the following -
class MyComponent extends React.Component {
componentWillMount() {
this.retrieveFromStorageAndRequestData();
}
async getItem(item) {
let response = await AsyncStorage.getItem(item);
// don't need await here since JSON.stringify is synchronous
let responseJson = JSON.stringify(response);
return responseJson;
}
async retrieveFromStorageAndRequestData = () => {
let assetId = await getItem(cmn.settings.property);
requestData({
fromDate: '20160601',
toDate: '20160701',
assetId,
url: '/abc/abc/xyz'
}) ;
}
// rest of the component
render() {
// render logic
}
}

Resources