The following code fetches a json list and then does another fetch call for each list item to change their values. The problem is that it’s not done synchronously. “new” is printed to the console before “update”.
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
data.forEach(function(e, index,array) {
fetch(API_URL_FOOD_DETAILS + e.foodid)
.then(response => response.json())
.then(data => {
array[index] = {...e, ...data};
console.log("update");
})
});
console.log("new", data)
});
Update
Here's how I incorporated #Andy's solution:
function fetchFoodDetails(id, index) {
return fetch(API_URL_FOOD_DETAILS + id)
.then(response => response.json())
.then(data => {
return [index, data];
});
}
function fetchDiary() {
return fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
return data;
})
}
(async () => {
const data = await fetchDiary();
console.log("old", JSON.stringify(data));
const promises = data.map((food, index) => fetchFoodDetails(food.id, index));
await Promise.all(promises).then(responses => {
responses.map(response => {
data[response[0]] = {...data[response[0]], ...response[1]};
console.log("update");
})
});
console.log('new', JSON.stringify(data));
})();
It was more difficult so I went with #connoraworden's solution. But I think it can be simplified.
Thanks for all your answers.
The best way to go about this is to use Promise.all() and map().
What map will do in this context return all the promises from fetch.
Then what will happen is await will make your code execution synchronous as it'll wait for all of the promise to be resolved before continuing to execute.
The problem with using forEach here is that it doesn't wait for asynchronous request to be completed before it moves onto the next item.
The code that you should be using here is:
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(async data => {
await Promise.all(data.map((e, index, array) => {
return fetch(API_URL_FOOD_DETAILS + e.foodid)
.then(response => response.json())
.then(data => {
array[index] = {...e, ...data};
console.log("update");
})
}));
console.log("new", data)
});
You shouldn't be using forEach here. The best solution is to use Promise.all which waits for an array of promises (fetch is a promise) to all resolve, after which you can process the data.
Here I've created a dummy fetch function with some sample data to quickly show you how that works.
const dummyObj = {
main: [ { id: 1 }, { id: 2 }, { id: 5 } ],
other: {
1: 'data1',
2: 'data2',
3: 'data3',
4: 'data4',
5: 'data5',
6: 'data6',
7: 'data7',
}
}
// The summy function simply returns a subset of the sample
// data depending on the type and id params after 2 seconds
// to mimic an API call
function dummyFetch(type, id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(id ? dummyObj[type][id] : dummyObj[type]);
}, 2000);
});
}
// In the first fetch we display the data, just
// like you did in your example
dummyFetch('main')
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
// Instead of a forEach use Array.map to iterate over the
// data and create a new fetch for each
const promises = data.map(o => dummyFetch('other', o.id));
// You can then wait for all promises to be resolved
Promise.all(promises).then((data) => {
// Here you would iterate over the returned group data
// (as in your example)
// I'm just logging the new data as a string
console.log(JSON.stringify(data));
// And, finally, there's the new log at the end
console.log("new", data)
});
});
Here's the async/await version:
const dummyObj = {
main: [ { id: 1 }, { id: 2 }, { id: 5 } ],
other: {
1: 'data1',
2: 'data2',
3: 'data3',
4: 'data4',
5: 'data5',
6: 'data6',
7: 'data7',
}
}
function dummyFetch(type, id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(id ? dummyObj[type][id] : dummyObj[type]);
}, 2000);
});
}
(async () => {
const oldData = await dummyFetch('main');
console.log("old", oldData);
const promises = oldData.map(o => dummyFetch('other', o.id));
const newData = await Promise.all(promises);
console.log(JSON.stringify(newData));
console.log('new', newData);
})();
fetch is a Promise. This is asyncronous call, so the "new" console.log runs before finished all the promises. Use Promise.all() for that.
You can do this so:
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
return Promise.all(data.map(food =>
fetch(API_URL_FOOD_DETAILS + food.foodid)
.then(resp => resp.json())
.then(json => {
// do some work with json
return json
})
))
})
.then(data => console.log('new', data))
Storing multiple responses in a single array
The following code fetches multiple keywords in queries and stores all the response of all three responses to the all array
let queries = ["food", "movies", "news"]
let all = []
queries.forEach((keyword)=>{
let [subres] = await Promise.all([fetch(`https://reddit.com/r/${keyword}/hot.json?limit=100`).then((response) => response.json())]);
all.push(subres)
})
//now you can use the data globally or use the data to fetch more data
console.log(all)
How to chain multiple fetch() promises?
You do it like how you have been doing it, just append another .then()
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
data.forEach(function(e, index,array) {
fetch(API_URL_FOOD_DETAILS + e.foodid)
.then(response => response.json())
.then(data => {
array[index] = {...e, ...data};
console.log("update");
})
.then(()=>{
console.log("new", data)
})
});
});
If you want show only once "console.log("new", data)", you can check it with the index, like this:
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
data.forEach(function(e, index,array) {
fetch(API_URL_FOOD_DETAILS + e.foodid)
.then(response => response.json())
.then(data => {
array[index] = {...e, ...data};
console.log("update");
if ((data.length - 1) === index) { // CHECK INDEX HERE IF IS THE LAST
console.log("new", data)
}
})
});
});
You will need a recursive function to do this.
fetch(API_URL_DIARY)
.then(response => response.json())
.then(data => {
console.log("old", data);
return data;
})
.then(data => {
recursiveFetch(data)
});
function recursiveFetch(initialData){
e = initialData[initialData.length-1]; //taking the last item in array
fetch(API_URL_FOOD_DETAILS + e.foodid)
.then(response => response.json())
.then(data => {
array[index] = {...e, ...data};
console.log("update");
initialData.pop() // removing last item from array, which is already processed
if(initialData.length > 0)
recursiveFetch(initialData)
})
}
Note: This is an untested code.
Related
In this case:
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(() => {
console.log("res1");
return new Promise((res) => res(1));
})
.then((res) => {
console.log(res);
});
fetch("https://jsonplaceholder.typicode.com/todos/2")
.then(() => {
console.log("res2");
return new Promise((res) => res(2));
})
.then((res) => {
console.log(res);
});
fetch("https://jsonplaceholder.typicode.com/todos/3")
.then(() => {
console.log("res3");
return new Promise((res) => res(3));
})
.then((res) => {
console.log(res);
});
The output is (i understand that the order here will depend on when it came first)
res1
1
res3
3
res2
2
But in this case:
Promise.resolve(1)
.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(res + " " + "first"));
})
.then((res) => {
console.log(res);
});
Promise.resolve(2)
.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(res + " " + "second"));
})
.then((res) => {
console.log(res);
});
Promise.resolve(3)
.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(res + " " + "third"));
})
.then((res) => {
console.log(res);
});
I'm getting the order I've been waiting for
1
2
3
1 first
2 second
3 third
My question is why with fetch the order will always be result + immediately next then and so on
And in common Promise.resolve I have 3 results and after next 3 then
then gets called when the associated promise resolves and the event loop isn't busy doing something else.
Promise.resolve(2) resolves immediately.
fetch takes time to receive the response over the network.
In this code I used multiple then() methods, I just want to convert it into only one then, How it is possible.
getGreeting = () => {
fetch(url)
.then((response) => response.json())
.then((result) => result.data)
.then((data) => printCards(data))
.catch((err) => {
console.log(err);
});
};
notice the async and await keywords
fetch(url)
.then(async (response) => {
const json = await response.json();
printCards(json.data);
})
.catch((err) => {
console.log(err);
});
};
There is such a method in a React application:
fetchData = async () => {
try {
const response = await fetch(`https://........`);
const data = (await response.json()).group;
this.setState({
data: data,
car: Object.keys(data)[0]
},this.filter);
} catch(err) {
console.log("404 Not Found");
}
};
How to write this part without async / await syntax, and using then and catch?
Just like this!
fetchData = () => {
fetch("YOUR_URL")
.then(response => response.json())
.then(json => json.group)
.then(data => {
this.setState({
data,
car: Object.keys(data)[0]
},this.filter);
})
.catch(err => {
console.log("404 Not Found");
});
}
It's simply-
fetchData = () => {
fetch(url)
.then(res => res.json())
.then(data => {
this.setState({data: data.group, car: Object.keys(data.group)[0]})
})
.catch(err => console.log(err));
};
If fetch returns a Promise you can:
fetchData = () => {
fetch(url)
.then(res => ... )
.catch(err => ...)
};
fetchData = () => {
return fetch(`https://........`)
.then(response => {
const data = response.json().group;
this.setState({
data: data,
car: Object.keys(data)[0]
},this.filter);
}).catch(err => {
console.log("404 Not Found");
});
};
response.json() won't return a Promise so you don't need await
I want to take data from JSON, next take another data from related JSON by ID and push it to my state array movies.
This is my code:
state = {
movies: []
}
componentDidMount() {
fetch('https://api.themoviedb.org/3/movie/popular?api_key=APIKEY&page=1')
.then(response => response.json())
.then(data => {
const movies = data.results;
movies.forEach(movie => this.moviePageAndGenres(movie.id, movie));
this.setState({
movies
});
})
}
moviePageAndGenres = (id, element) => {
fetch('https://api.themoviedb.org/3/movie/' + id + '?api_key=APIKEY')
.then(response => response.json())
.then(data => {
element.genres = data.genres;
element.homepage = data.homepage;
});
}
In render() I just console.log my movies to check if data inside is correct.
Output:
image
So it's correct but when I check Component Props these props are not transferred.
image
This is how I transfer props:
const movies = this.state.movies.map(movie =>
<Movie genres={movie.genres}
homepage={movie.homepage}
key={movie.id}
title={movie.title}
poster={movie.poster_path}
rating={movie.vote_average}
/>
)
I guess it's problem with multiple call of asynchronousfetch(). But i don't know how to handle with it.
The reason its not working is, you are firing multiple fetch calls which are async and setting the state immediately after it. setState will get empty movies in that case.
fetch api returns a promise and you should set your state in promise resolution handler. Modify your componentDidMount like this.
componentDidMount() {
fetch('https://api.themoviedb.org/3/movie/popular?api_key=APIKEY&page=1')
.then(response => response.json())
.then(data => {
const movies = data.results;
Promise.all(movies.map(movie => fetch(
`https://api.themoviedb.org/3/movie/${movie.id}?api_key=APIKEY`
)))
.then(resp => Promise.all( resp.map(r => r.json()) ))
.then(result => {
const movies = result.map((data, i) => {
const movie = Object.assign(movies[i], {
genres: data.genres,
homepage: data.homepage
});
return movie;
});
this.setState({
movies
});
});
})
}
You need async await in this case and it’s good to use Promise.all because you are doing fetch in forEach.
For forEach you need await Promise.all and for fetch you need await. Which mean it will wait until the forEach is completed
Change
fetch('https://api.themoviedb.org/3/movie/popular?api_key=APIKEY&page=1')
.then(response => response.json())
.then(data => {
const movies = data.results;
movies.forEach(movie => this.moviePageAndGenres(movie.id, movie));
this.setState({
movies
});
})
To
fetch('https://api.themoviedb.org/3/movie/popular?api_key=APIKEY&page=1')
.then(response => response.json())
.then(async data => {
const movies = data.results;
await Promise.all(movies.forEach(async movie => await this.moviePageAndGenres(movie.id, movie)))
this.setState({
movies
});
})
Also
Change
moviePageAndGenres = (id, element) => {
fetch('https://api.themoviedb.org/3/movie/' + id + '?api_key=APIKEY')
.then(response => response.json())
.then(data => {
element.genres = data.genres;
element.homepage = data.homepage;
});
}
To
moviePageAndGenres = async (id, element) => {
return await fetch('https://api.themoviedb.org/3/movie/' + id + '?api_key=APIKEY')
.then(response => response.json())
.then(data => {
element.genres = data.genres;
element.homepage = data.homepage;
});
}
I have following React codes:
fetchA() {
fetch(aURL)
.then(response => response.json())
.then(json => {
this.setState({ a: json})
})
}
fetchB() {
fetch(bURL)
.then(response => response.json())
.then(json => {
this.setState({ b: json})
})
}
Most tutorials in the web teach you how to wait until responses of both A and B been collected such as:
Promise.all([fetchA, fetchB])
.then(values => {
console.log(values[0])
console.log(values[1])
})
But what I want is fetch A, when A finished<results been colleted> then fetch B.
How could I do that?
Thanks for your time!
====== edited =======
In my real codes, I tried with:
fetchA(){
fetch(aURL)
.then(fetchB())
.then(response => console.log(response))
.then(() => console.log('hi'))
}
fetchB(){
fetch(bURL)
.then((response) => console.log(response))
}
// call it
this.fetchA()
// output:
//
// A data
// hi
// B data
// what I want is:
//
// B data
// A data
// hi
You can use then to chain it.
fetch(aURL)
.then(response = > console.log('do something'))
.then(fetchB)
Update: don't forget to correct fetchB. It should return Promise.
fetchB() {
return fetch(bURL)
.then(response => response.json())
.then(json => {
this.setState({ b: json})
})
}
fetchA() {
fetch(aURL)
.then(response => response.json())
.then(json => {
this.setState({ a: json}, this.fetchB); //this line changed
})
}
and call this.fetchA() to start