setState doesn t work in a promise. State undefined with React - reactjs

The problem is that I retrieve links from photos stored in firebase storage, I retrieve the links in an array (arrayurl) and everything works fine (console.log (arrayurl) shows me the link fix well) until I update the status with this.setstate ({array: arrayurl}). There is already the crash since it tells me that array is undefined. I know it's a sync problem but I can't get the array state to change in the promise. Try to move the setstate from place but always the same ...thanks for your help!
recupurl() {
var arrayurl = []
var storageRef = firebase.storage();
var listRef = storageRef.ref('images');
listRef.listAll().then((res) => {
res.items.forEach((itemRef) => {
var listRef2 = storageRef.ref('images').child(itemRef.name)
listRef2.getDownloadURL().then((url) => {
arrayurl.push(url);
console.log(arrayurl);
this.setState({
array: arrayurl
})
});
})
})
}

This happens when this does not mention class.
To solve this problem, I define a variable outside the block and pour this into it and use it inside my own block.
for example :
let T = this
let urls =[]
let storageRef = firebase.storage();
let listRef = storageRef.ref('images');
listRef.listAll().then( res => {
res.items.forEach( itemRef => {
let _listRef = storageRef.ref('images').child(itemRef.name)
_listRef.getDownloadURL().then( url => {
urls.push(url);
console.log(urls);
T.setState({array:arrayurl})
});
})})}

You're mutating the state because of using the same array. The state should be updated in an immutable way. You also seem to have a problem with accessing setState because of this keyword referring to something other than the class, you can use an arrow function for that.
If you only want to update the state when the array is fully fetched instead of for each iteration, you can use this:
recupurl = () => {
const arrayurl = []
const storageRef = firebase.storage();
const listRef = storageRef.ref('images');
listRef.listAll().then((res) => {
res.items.forEach((itemRef) => {
const listRef2 = storageRef.ref('images').child(itemRef.name)
arrayurl.push(listRef2.getDownloadURL())
});
Promise.all(arrayurl).then(urlArr => {
this.setState({array: urlArr })
}).catch(error => {
console.log(error)
})
})
}

Related

getDownloadURL in array of dictionary (re-rendering issue, forEach)

I have an array of dictionaries, (e.g. [{}, {}, {}, {}], each dictionary contains information about book)
I want to download image from firebase storage using getDownloadURL.
My current code's like...
const [resObj, setresObj] = useState() // empty variable for update state
let result = [] //create empty array for copy & push new obj
useEffect(() => {
props.resObj.forEach((obj) => { // props.resObj: array of dictionary I explained before
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
getDownloadURL(imgRef)
.then((url) => {
result1.push({
...obj,
bookUrl: url
}) // copy & push dictionary
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
setresObj(result1)
}, [])
after this code update 'resObj' variable,
I map resObj in component like...
return (
<div>
{resObj? resObj.map(item => {
<img
key = {}
className = '~~'
onClick = {}
src = {item.bookUrl}
/>
})}
</div>
)
unfortunately.. it doesn't show nothing..
It seems that forEach, useEffect, useState, getDownloadURL Promise seriously entangled..
I tried 1) devide download image code as function, 2) devide download image code as recoil, 3) escape download image code from useEffect, 4) ...(extra variances of code)...
The problem is that your call to setresObj happens before any of the calls to result1.push have happened, so you're always setting an empty array. It's easiest to verify this by setting breakpoints and running in the debugger, or by adding some console.log calls.
The fix is to use Promise.all to wait for all download URLs to have been retrieved and only then call setresObj. Something like this:
useEffect(() => {
let promises = props.resObj.map((obj) => {
const jpgName = 'bookDB/'+ obj.도서번호 + '.jpg';
const imgRef = ref(storage, jpgName)
return getDownloadURL(imgRef)
.then((url) => {
return {
...obj,
bookUrl: url
}
})
.catch((error) => {
if (error.code === 'storage/object-not-found') {
console.log('이미지 파일 없음')
result1.push({
...obj,
bookUrl: "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg"
})
} else { console.log(error)}
})
})
Promise.all(promises).then((results) => {
setresObj(results);
});
}, [])

How could I write this function so it doesn't setState within the foreach everytime

The function collects role Assignment PrincipalIds on an item in SPO. I then use a foreach to populate state with the Title's of these PrincipalIds. This all works fine but it's inefficient and I'm sure there is a better way to do it than rendering multiple times.
private _permJeChange = async () => {
if(this.state.userNames){
this.setState({
userNames: []
});
}
var theId = this.state.SelPermJEDD;
var theId2 = theId.replace('JE','');
var info = await sp.web.lists.getByTitle('MyList').items.getById(theId2).roleAssignments();
console.log(info, 'info');
var newArr = info.map(a => a.PrincipalId);
console.log(newArr, 'newArr');
// const userIds = [];
// const userNames = [];
// const userNameState = this.state.userNames;
newArr.forEach(async el => {
try {
await sp.web.siteUsers.getById(el).get().then(u => {
this.setState(prevState => ({
userNames: [...prevState.userNames, u.Title]
}));
// userNames.push(u.Title);
// userIds.push(el);
});
} catch (err) {
console.error("This JEForm contains a group");
}
});
}
I've left old code in there to give you an idea of what I've tried. I initially tried using a local variable array const userNames = [] but declaring it locally or even globally would clear the array everytime the array was populated! So that was no good.
PS. The reason there is a try catch is to handle any SPO item that has a permissions group assigned to it. The RoleAssignments() request can't handle groups, only users.
Create an array of Promises and await them all to resolve and then do a single state update.
const requests = info.map(({ PrincipalId }) =>
sp.web.siteUsers.getById(PrincipalId).get().then(u => u.Title)
);
try {
const titles = await Promise.all(requests);
this.setState(prevState => ({
userNames: prevState.userNames.concat(titles),
}));
} catch (err) {
console.error("This JEForm contains a group");
}

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

react js, why is this undefined

I have this array that is type Technology and type undefined and its making things very difficult. For example I am trying to compare this array to another array and it complains that it might be undefined etc. So I am wondering why is this undefined and how to prevent this.
This component is my main component that gets the data from the server and creates 2 different arrays holding all the data from each collection.
const App = ():JSX.Element => {
//Data from server recieve both collections in one get
const onResponse = (result:JSONData):void => {
// data received from Web API
//console.table(result);
setTechnologies(result.technologies);
setAll_Courses(result.all_courses);
setLoading(false);
};
//For error
const onError = (message:string):void => console.log("*** Error has occured during AJAX data transmission: " + message);
// ----------------------------------- setting state variables
const [technologies, setTechnologies] = React.useState<Technology[]>([]);
const [all_courses, setAll_Courses] = React.useState<AllCourses[]>([]);
// Loading
const [loading, setLoading] = React.useState<boolean>(false);
// Routing and history
const history:any = useHistory();
const route:string = useLocation().pathname;
React.useEffect(():void => {
getJSONData(COURSE_SCRIPT, onResponse, onError);
}, []);
And in my other component I make an array from the data that matches this _id in the collection of technologies. So I can then work in this component with that specific document, because I need to edit the data and display data etc. Everything is difficult because its undefined.
const EditTechnology = ({technologies, all_courses, visible}:ViewProps):JSX.Element => {
let { id } = useParams<{id:string}>();
let edit_Technology:(Technology | undefined) = technologies.find(item => item._id === id) ;
you can give props a default value in child components to prevent undefined errors, like this
const EditTechnology = ({technologies = [], all_courses = [], visible}:ViewProps):JSX.Element => {
let { id } = useParams<{id:string}>();
let edit_Technology:(Technology | undefined) = technologies.find(item => item._id === id);
}
Or put it in onResponse, in this way, you don’t need to add many default value to each child components
const onResponse = (result:JSONData):void => {
// data received from Web API
//console.table(result);
setTechnologies(result.technologies || []);
setAll_Courses(result.all_courses || []);
setLoading(false);
};
I think it maybe depends on method find
const bad = ['you','will','get', 'undefined', 'if', 'you', 'looking']
.find(e => e === 'something else')
const good = ['now', 'you', 'will','find', 'it']
.find(e => e === 'it')
console.log('for `something else` -', bad)
console.log('for `it` -', good)

How to Loop an Array Using Map Function Based on Condition and Then setState?

I want to have same functionality using map function. Following is my code:
async componentDidMount() {
const data = await TodoAPI.getTodo();
for(var i=0;i<data.response.length;i++)
{
if(data.response[i]._id === this.props.match.params.id)
{
this.setState({
todo_description: data.response[i].todo_description,
todo_responsible: data.response[i].todo_responsible,
todo_priority: data.response[i].todo_priority,
todo_completed: data.response[i].todo_completed
})
}
}
}
I guess you’re a bit confused, as map() would not be useful in this case. It creates a new array but what you want (correct me if I’m wrong) is to find the item, so find() seems to be what you need:
async componentDidMount() {
const data = await TodoAPI.getTodo();
const id = this.props.match.params.id;
const item = data.response.find(item => item._id === id);
this.setState(item);
}

Resources