ReactJS setState from asynchronous callback - javascript

Hey ReactJs Community,
I am fairly new to ReactJs and have gotten my first components set up. Now I'm at a point where I would like to update all state items on a specific event. I am looping each state item via map() and am calling a asynchronous method to determine a value to be included in the state.
This method is returning GET data via the callback function.
updateItems: function() {
var items = this.state.items;
items.map(function(item, i) {
checkConfirmation(item.hash, function(confirmations) {
console.log("Item " + i + " has " + confirmations + " confirmations!");
items[i].hash = item.hash + " (" + confirmations + ")";
items[i].completed = true;
});
});
}
How can I update my state from the asynchronous callback?
I tried passing in this as the second parameter in map() and I tried calling setState after the map() function, but that cannot work since it will be called before any data is returned in the callback.
Thank you for taking time to help me with this issue!

You can make a promise for each pending request. Await them all using Promise.all. And set state when all requests are done.
updateItems: function() {
const items = this.state.items;
const pending = items.map(item => new Promise(resolve => {
checkConfirmation(item.hash, resolve)
}))
Promise.all(pending)
.then(confirmations => confirmations.map((confirmation, i) => {
const item = items[i]
// copy items mutating state is bad
return Object.assign({}, item, {
completed: true,
hash: `${item.hash}(${confirmation})`
})
}))
.then(items => this.setState({ items })
}
UPD Callbacks hackery
updateItems: function() {
const items = this.state.items;
let confirmations = []
let loaded = 0
const addConfirmation = i => confirmation => {
confirmations[i] = confirmation
loaded++;
// if all were loaded
if(loaded === items.length) allConfirmationsLoaded()
}
const allConfirmationsLoaded = () => {
const newItems = confirmations.map((confirmation, i) => {
const item = items[i]
// copy items mutating state is bad
return Object.assign({}, item, {
completed: true,
hash: `${item.hash}(${confirmation})`
})
})
this.setState({items: newItems})
}
// for each item lauch checkConfirmation
items.forEach((item, i) => {
checkConfirmation(item.hash, addConfirmation(i))
})
}

Related

expo | only changes on state while call arrow-function?

i have a problem with following function;
const handleSave = (task, description, priorityState, taskID) => {
changeLabel(task, taskID);
changeDescription(description, taskID);
changePriority(priorityState, taskID);
navigation.goBack();
};
the problem is thats only change the last used function:
if i'll change the Label and the Description its only save the Description for example. <
i call the function while clicking a Button (TouchableOpacity)
<Button
title={language.edit.save}
color={theme.secondary}
onPress={() => {
handleSave(task, description, priorityState, taskID);
}}
/>
any advise?
what did i try?
delay the functions:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const handleSave = (task, description, priorityState, taskID) => {
changeLabel(task, taskID);
sleep(10);
changeDescription(description, taskID);
sleep(10); ...
};
i would like to thank any kind of help <3
full code here.
Your 3 custom functions changeLabel, changeDescription and changePriority all try to update the same tasks state:
Repo source
const [tasks, setTasks] = useState([]);
const changePriority = (color, taskID) => {
const task = tasks.map(/* etc. */);
setTasks(task);
};
const changeDescription = (description, taskID) => {
const task = tasks.map(/* etc. */);
setTasks(task);
};
const changeLabel = (value, taskID) => {
const task = tasks.map((/* etc. */);
setTasks(task);
};
When you successively call these functions, each one reads the current tasks state, which has not been updated yet by the previous call to setTasks SetStateAction. See also The useState set method is not reflecting a change immediately
You can easily solve this by using the functional update form of the SetStateAction:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
In your case, change your 3 functions to get the previous state as a parameter:
const changePriority = (color, taskID) => {
setTasks(
// Use previous tasks received as argument
// instead of directly tasks which may be
// an old value.
(previousTasks) => previousTasks.map(/* etc. */)
);
};
// Same for the other 2 functions.
The solution from #ghybs worked. I appreciate your work 👍 Thx
i changed the function from:
const markTask = (taskID) => {
const task = tasks.map((item) => {
if (item.id == taskID) {
return { ...item, completed: true };
}
return item;
});
setTasks(task);
};
to this:
const changeLabel = (value, taskID) => {
setTasks((previousTasks) =>
previousTasks.map((item) => {
if (item.id == taskID) {
return { ...item, label: value };
}
return item;
})
);
};
snack with the issue

React: how to wait for map function to complete before updating state?

I have an array of ids, stored in ids, that I'm mapping over an async function, asyncFunc. I want to store the result of the mapping into newArr and then update state via setState. Here's my code:
useEffect(() => {
const newArr = [];
ids.map((element) => {
asyncFunc(variables).then((data) => {
newArr.push({ layer: makeLayer(data) });
});
});
setState([...layers, ...newArr]);
}, [variables]);
My issue is that the state is updating before the mapping completes. How can I use .then() to update the state only when data from all the mappings were returned? Wrapping ids.map with Promise.all didn't work. In addition, the following returns an error:
useEffect(() => {
const newArr = [];
(ids.map((element) => {
asyncFunc(variables).then((data) => {
newArr.push({ layer: makeLayer(data) });
});
})).then(()=> {
setState([...layers, ...newArr]);
})
}, [variables]);
Declare a async function to populate the array and return your array within a Promise.all. Something like this should work for what you're trying to accomplish:
useEffect(() => {
const populateArray = async () => {
const newArr = await Promise.all(ids.map(async element => {
const data = await asyncFunc(variables)
return { layer: makeLayer(data) }
})
setState([...layers, ...newArr]);
}
populateArray()
}, [variables]);

variable outside foreach is null but has a value inside it

I have this reactjs function to get data from firebase.
The problem is the postsArray. It inside foreach has the objects, outside it is null.
I add postsArray globally.
Any ideas why the return is null?
postsRef.on('value', function(snapshot) {
setPosts(prevPosts => {
snapshot.forEach((subChild) => {
var value = subChild.val();
value = value.postID;
var post = firebase.database().ref('/posts/' + value);
post.on('value', function(snapshot2) {
postsArray = snapshot2.val();
console.log(postsArray); // HAS THE VALUE
});
console.log(postsArray); // NO VALUE HERE.
});
return [...prevPosts, ...Object.keys(postsArray).reverse().map(key => ({
key: key, ...postsArray[key]
}))];
post.on is async function that is why the value outside of the forEach loop is undefined for postArray
The solution here is to set state inside post.on. This however will lead to multiple setState and also keep adding the postArray data state.
postsRef.on('value', function(snapshot) {
snapshot.forEach((subChild) => {
var value = subChild.val();
value = value.postID;
var post = firebase.database().ref('/posts/' + value);
post.on('value', function(snapshot2) {
postsArray = snapshot2.val();
setPosts(prevPosts => {
return [...prevPosts, ...Object.keys(postsArray).reverse().map(key => ({
key: key, ...postsArray[key]
}))];
})
});
})
})
A better solution here is to convert your callback syntax to use promise. I am not sure if firebase.database provides a promise format so I will just show you the traditional way using Promise.all and new Promise
postsRef.on('value', async function(snapshot) {
const promises = []
snapshot.forEach((subChild) => {
var value = subChild.val();
value = value.postID;
var post = firebase.database().ref('/posts/' + value);
promises.push(new Promise((res, rej) => {
post.on('value', function(snapshot2) {
res(snapshot2.val())
});
}));
})
const postArray = await Promise.all(promises);
setPosts(prevPosts => {
return [
...prevPosts,
...Object.keys(postsArray).reverse().map(key => ({
key: key, ...postsArray[key]
}))
];
})
})
post.on() is an asynchronous event listener, your current code is synchronous. If you post more of your code I can help you restructure it.
What is happening is:
var post = firebase.database().ref('/posts/' + value);
var postFunction = function(snapshot2) {
postsArray = snapshot2.val();
console.log(postsArray); // HAS THE VALUE
};
// set the post on function. This isn't calling the function yet.
post.on('value', postFunction);
// No value here because the post function has not run yet.
console.log(postsArray); // NO VALUE HERE.
// the postFunction post.on is called so now you will get the console.
I would change it to look like this:
const addNewPostsToPrevious = (prevPosts) => (newPosts) => {
return [
...prevPosts,
...Object.keys(newPosts)
.reverse()
.map((key) => ({
key,
...newPosts[key],
})),
];
};
postsRef.on('value', (snapshot) => {
setPosts((prevPosts) => {
snapshot.forEach((subChild) => {
const post = firebase.database().ref(`/posts/${subChild.val().postID}`);
post.on('value', (snapshot2) => {
addNewPostsToPrevious(prevPosts)(snapshot2.val());
});
});
});
});

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

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,
}
})
}

Javascript. Wait for nested fetch promises

So i need to use the result of a promise to make another fectch request, i'm working with the rest api for wordpress, and i need the id of subcategories inside the post object to retrieve that category name and build two arrays one with the posts and another with categories names availables.
Here is my function
function fetchAccordionData()
{
const id = document.querySelector('.acc').getAttribute('data-id'),
wpRestAPI = '/wp-json/wp/v2/';
return fetch(wpRestAPI + 'posts?per_page=100&categories=' + id)
.then((resp) => resp.json())
.then((data) =>
{
let curId = [], subCatList = [];
data.map((post) =>
{
let catList = post.categories.filter((c) => c !== parseInt(id));
fetch(wpRestAPI + 'categories/' + catList[0])
.then((r) => r.json())
.then((cat) =>
{
if(!curId.includes(cat.id)) subCatList.push({id: cat.id, name: cat.name});
curId.push(cat.id);
});
});
return {'subCatList':subCatList, 'posts':data}
});
}
Now when i call the function the subCatListarray isn`t ready yet:
fetchAccordionData().then((data) =>
{
console.log(data.subCatList, data.posts);
for(let cat of data.subCatList)
{
console.log(cat);
}
});
So, how do i know when the promise of the second fetch is resolved so i can use the data?
You'll need to place all of your promises in an array and use Promise.all to wait for all of those promises to resolve before accessing subCatList.
Your modified code would look like so:
function fetchAccordionData() {
const id = document.querySelector('.acc').getAttribute('data-id'),
wpRestAPI = '/wp-json/wp/v2/';
return fetch(wpRestAPI + 'posts?per_page=100&categories=' + id)
.then((resp) => resp.json())
.then((data) => {
let curId = [], subCatList = [];
// promises is an array of promises
let promises = data.map((post) => {
let catList = post.categories.filter((c) => c !== parseInt(id));
// return a promise on each iteration
return fetch(wpRestAPI + 'categories/' + catList[0])
.then((r) => r.json())
.then((cat) =>
{
if(!curId.includes(cat.id)) subCatList.push({id: cat.id, name: cat.name});
curId.push(cat.id);
});
});
return Promise.all(promises)
.then(() => ({'subCatList':subCatList, 'posts':data}));
});
}
Notice that the last step returns the object {'subCatList': subCatList, 'post': data} only after every promise in promises has resolved. That way, you can be confident that the promises in the array are finished making their push into subCatList.
It's also worth noting that the interface of fetchAccordionData stayed exactly the same, so you should be able to use it as you did in your original example:
fetchAccordionData().then((data) => {
console.log(data.subCatList, data.posts);
for(let cat of data.subCatList) {
console.log(cat);
}
});
It looks like
return {'subCatList':subCatList, 'posts':data}
is outside of the part of your function that gets categories from the rest API:
data.map((post) =>
{
let catList = post.categories.filter((c) => c !== parseInt(id));
fetch(wpRestAPI + 'categories/' + catList[0])
.then((r) => r.json())
.then((cat) =>
{
if(!curId.includes(cat.id)) subCatList.push({id: cat.id, name: cat.name});
curId.push(cat.id);
});
});
so your function is returning it before it can fetch the category data from the API. If you put your return statement after the last then statement it should return the data your are looking for:
data.map((post) =>
{
let catList = post.categories.filter((c) => c !== parseInt(id));
fetch(wpRestAPI + 'categories/' + catList[0])
.then((r) => r.json())
.then((cat) =>
{
if(!curId.includes(cat.id)) subCatList.push({id: cat.id, name: cat.name});
curId.push(cat.id);
return {'subCatList':subCatList, 'posts':data}
});
});

Categories

Resources