How to fetch data over multiple pages? - javascript

My project is based on React, redux, redux-saga, es6 and I try to fetch data from this API:
http://api.dhsprogram.com/rest/dhs/data/BD,2000,2004,2007?&returnFields=CharacteristicLabel,Indicator,IndicatorId,Value&f=json
As you can see, this specific API call shows data with a limit of 100 data per page spread on 40 pages.
According to this answer:
http://userforum.dhsprogram.com/index.php?t=msg&th=2086&goto=9591&S=Google
it says that you can extend the limit to a maximum of 3000data per page.
However, in some cases I would do an API call that exceeds that limit which means I would not receive all my data doing it like this:
export function fetchMetaData(countryCode: string, surveyYears: string) {
return (fetch('http://api.dhsprogram.com/rest/dhs/data/' + countryCode + ',' + surveyYears + '?returnFields=CharacteristicLabel,Indicator,IndicatorId,Value&f=json')
.then(response => response.json())
.then(json => json.Data.map(survey => survey)))
}
So my question is; what is the best way to get all data from this API given that I know the total pages of data. The answer in the forum link suggest to loop through the API. However, I can't find the right syntax usage to do this.
My idea would be doing one api call to get the total number of pages. Then store this in a state using redux+redux-saga. Then do a new request sending the total pages as parameter and fetch this total number of pages times. And by doing this I can't figure out the syntax to store the data for each iteration.

A possible solution -the idea is to get the number of pages first, then make the appropriate number of API calls, pushing the promise from each call into an array. We then wait for all the promises to resolve, and do something with the returned data.
async function fetchMetaData() {
const response = await fetch('apiUrlToGetPageNumber');
const responses = await Promise.all(
Array.from(
Array(resp.data.pagesRequired),
(_, i) => fetch(`apiUrlToSpecificPage?page=${i}`)
)
);
// do something with processedResponses here
}

Here is another possible solution using async/await. The beauty of this is that the total_pages count is dynamic, so that if it increases while you're processing your request, it'll make sure you get it all.
async function fetchMetaData() {
let allData = [];
let morePagesAvailable = true;
let currentPage = 0;
while(morePagesAvailable) {
currentPage++;
const response = await fetch(`http://api.dhsprogram.com/rest/dhs/data?page=${currentPage}`)
let { data, total_pages } = await response.json();
data.forEach(e => allData.unshift(e));
morePagesAvailable = currentPage < total_pages;
}
return allData;
}

Related

Axios POST request in cycle

I need to save a list of dishes in my restaurant's menu. I do it like this:
fromFormMenu.forEach(fromFormMeal => {
MealAPI.create(fromFormMeal, restaurant.id).then(resp => console.log(resp))
})
In this case, the dishes are saved to the database in the wrong order in which the user entered them into the form. Here is what my API method looks like:
static async create(meal, restaurantId) {
const response = await axios.post(REST_URL + "/" + restaurantId, meal);
return response
}
It is necessary for me that the order of records was saved such as they were entered by the user. I assume that the problem is related to the asynchrony of requests, because of which the records are stored in random order. I even tried removing the 'async' in the method declaration, but that didn't work.
The reason of getting the wrong order is you're calling all the api at the same time. If some of them is finished faster than the previous items, it will be saved before them.
So in this situation you might wanna call the next api after the first one is finished.
Something like:
const mainFunction = async () => {
for (let i = 0; i < fromFormMenu.length; i++) {
const resp = await MealAPI.create(fromFormMeal, restaurant.id)
console.log(resp)
}
}

How to get data from the links in an API?

I need to get some data from this API-page: https://swapi.dev/api/films/2.
Specifically, I need to get the name, gender, and date of birth of each character from the second episode of Star Wars.
Without problems I can get the main data of the film, such as the title, the director, the producer and so on. However, I cannot get the data of the individual characters.
I tried to do this with just one function and thought it would be useful to create a for loop. The loop should fetch each result and then return the searched data to me.
async function GetCharacters() {
const people = fetch("https://swapi.dev/api/films/2/").then(data => {
return data.json();
}).then(films => {
console.log(films.characters)
})
for (let i = 0; i < people.length; i++) {
const request = await fetch(film.characters[i]);
const character = await request.json();
console.log(character.name)
console.log(character.gender)
console.log(character.birth_year)
}
}
GetCharacters()
However, the result I get is different from what I would like:
['https://swapi.dev/api/people/1/', 'https://swapi.dev/api/people/2/', 'https://swapi.dev/api/people/3/', 'https://swapi.dev/api/people/4/', 'https://swapi.dev/api/people/5/', 'https://swapi.dev/api/people/10/', 'https://swapi.dev/api/people/13/', 'https://swapi.dev/api/people/14/', 'https://swapi.dev/api/people/18/', 'https://swapi.dev/api/people/20/', 'https://swapi.dev/api/people/21/', 'https://swapi.dev/api/people/22/', 'https://swapi.dev/api/people/23/', 'https://swapi.dev/api/people/24/', 'https://swapi.dev/api/people/25/', 'https://swapi.dev/api/people/26/']
Instead of getting the searched data for each character, I get an array of links, as if the loop didn't work. Why is that?
There's two issues here. Firstly, the people variable holds a Promise object, not the data returned from the AJAX request. Therefore your for loop is not executing. You need to perform the loop through the characters array within the callback.
Secondly, you're using await in the arrow function which is not async. To fix this you can use the same fetch().then() pattern within the loop to retrieve character information:
function GetCharacters() {
fetch("https://swapi.dev/api/films/2/").then(d => d.json()).then(films => {
films.characters.forEach(url => {
fetch(url).then(d => d.json()).then(character => {
console.log(character.name, character.gender, character.birth_year);
// work with the character data here...
})
})
})
}
GetCharacters()

Array of filtered axios results from paginated API is empty

In my code below I get an empty array on my console.log(response) but the console.log(filterdIds) inside the getIds function is showing my desired data. I think my resolve is not right.
Note that I run do..while once for testing. The API is paged. If the records are from yesterday it will keep going, if not then the do..while is stopped.
Can somebody point me to the right direction?
const axios = require("axios");
function getToken() {
// Get the token
}
function getIds(jwt) {
return new Promise((resolve) => {
let pageNumber = 1;
const filterdIds = [];
const config = {
//Config stuff
};
do {
axios(config)
.then((response) => {
response.forEach(element => {
//Some logic, if true then:
filterdIds.push(element.id);
console.log(filterdIds);
});
})
.catch(error => {
console.log(error);
});
} while (pageNumber != 1)
resolve(filterdIds);
});
}
getToken()
.then(token => {
return token;
})
.then(jwt => {
return getIds(jwt);
})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
I'm also not sure where to put the reject inside the getIds function because of the do..while.
The fundamental problem is that resolve(filterdIds); runs synchronously before the requests fire, so it's guaranteed to be empty.
Promise.all or Promise.allSettled can help if you know how many pages you want up front (or if you're using a chunk size to make multiple requests--more on that later). These methods run in parallel. Here's a runnable proof-of-concept example:
const pages = 10; // some page value you're using to run your loop
axios
.get("https://httpbin.org") // some initial request like getToken
.then(response => // response has the token, ignored for simplicity
Promise.all(
Array(pages).fill().map((_, i) => // make an array of request promisess
axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${i + 1}`)
)
)
)
.then(responses => {
// perform your filter/reduce on the response data
const results = responses.flatMap(response =>
response.data
.filter(e => e.id % 2 === 0) // some silly filter
.map(({id, name}) => ({id, name}))
);
// use the results
console.log(results);
})
.catch(err => console.error(err))
;
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
The network tab shows the requests happening in parallel:
If the number of pages is unknown and you intend to fire requests one at a time until your API informs you of the end of the pages, a sequential loop is slow but can be used. Async/await is cleaner for this strategy:
(async () => {
// like getToken; should handle err
const tokenStub = await axios.get("https://httpbin.org");
const results = [];
// page += 10 to make the snippet run faster; you'd probably use page++
for (let page = 1;; page += 10) {
try {
const url = `https://jsonplaceholder.typicode.com/comments?postId=${page}`;
const response = await axios.get(url);
// check whatever condition your API sends to tell you no more pages
if (response.data.length === 0) {
break;
}
for (const comment of response.data) {
if (comment.id % 2 === 0) { // some silly filter
const {name, id} = comment;
results.push({name, id});
}
}
}
catch (err) { // hit the end of the pages or some other error
break;
}
}
// use the results
console.log(results);
})();
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Here's the sequential request waterfall:
A task queue or chunked loop can be used if you want to increase parallelization. A chunked loop would combine the two techniques to request n records at a time and check each result in the chunk for the termination condition. Here's a simple example that strips out the filtering operation, which is sort of incidental to the asynchronous request issue and can be done synchronously after the responses arrive:
(async () => {
const results = [];
const chunk = 5;
for (let page = 1;; page += chunk) {
try {
const responses = await Promise.all(
Array(chunk).fill().map((_, i) =>
axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${page + i}`)
)
);
for (const response of responses) {
for (const comment of response.data) {
const {name, id} = comment;
results.push({name, id});
}
}
// check end condition
if (responses.some(e => e.data.length === 0)) {
break;
}
}
catch (err) {
break;
}
}
// use the results
console.log(results);
})();
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
(above image is an except of the 100 requests, but the chunk size of 5 at once is visible)
Note that these snippets are proofs-of-concept and could stand to be less indiscriminate with catching errors, ensure all throws are caught, etc. When breaking it into sub-functions, make sure to .then and await all promises in the caller--don't try to turn it into synchronous code.
See also
How do I return the response from an asynchronous call? and Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference which explain why the array is empty.
What is the explicit promise construction antipattern and how do I avoid it?, which warns against adding a new Promise to help resolve code that already returns promises.
To take a step back and think about why you ran into this issue, we have to think about how synchronous and asynchronous javascript code works together. Your
synchronous getIds function is going to run to completion, stepping through each line until it gets to the end.
The axios function invocation is returning a Promise, which is an object that represents some future fulfillment or rejection value. That Promise isn't going to resolve until the next cycle of the event loop (at the earliest), and your code is telling it to do some stuff when that pending value is returned (which is the callback in the .then() method).
But your main getIds function isn't going to wait around... it invokes the axios function, gives the Promise that is returned something to do in the future, and keeps going, moving past the do/while loop and onto the resolve method which returns a value from the Promise you created at the beginning of the function... but the axios Promise hasn't resolved by that point and therefore filterIds hasn't been populated.
When you moved the resolve method for the promise you're creating into the callback that the axios resolved Promise will invoke, it started working because now your Promise waits for axios to resolve before resolving itself.
Hopefully that sheds some light on what you can do to get your multi-page goal to work.
I couldn't help thinking there was a cleaner way to allow you to fetch multiple pages at once, and then recursively keep fetching if the last page indicated there were additional pages to fetch. You may still need to add some additional logic to filter out any pages that you batch fetch that don't meet whatever criteria you're looking for, but this should get you most of the way:
async function getIds(startingPage, pages) {
const pagePromises = Array(pages).fill(null).map((_, index) => {
const page = startingPage + index;
// set the page however you do it with axios query params
config.page = page;
return axios(config);
});
// get the last page you attempted, and if it doesn't meet whatever
// criteria you have to finish the query, submit another batch query
const lastPage = await pagePromises[pagePromises.length - 1];
// the result from getIds is an array of ids, so we recursively get the rest of the pages here
// and have a single level array of ids (or an empty array if there were no more pages to fetch)
const additionalIds = !lastPage.done ? [] : await getIds(startingPage + pages, pages);
// now we wait for all page queries to resolve and extract the ids
const resolvedPages = await Promise.all(pagePromises);
const resolvedIds = [].concat(...resolvedPages).map(elem => elem.id);
// and finally merge the ids fetched in this methods invocation, with any fetched recursively
return [...resolvedIds, ...additionalIds];
}

Which is performant multiple request for single doc or single request for 500000 docs (Cloud Firestore)?

I want to fetch 50 users from cloud firestore and I have two way both works.
But actually I dont know which is performant, as we have a poor internet connection in our country, if our focus be only fetching not iterating.
The first way (Single Request)
let tempList = [];
const matchingUsers = [user1, user2, user3, ..., user50];
const snap = await db.collection('users').get();
if (snap.size > 0) {
sanp.docs.forEach(doc => {
const data = doc.data();
matchingUsers.forEach(user => {
if (data.user === user) {
tempList.push(data.user);
}
});
});
}
The second way (multiple request)
matchingUsers.forEach(async user => {
const snap = await db.collection('users').doc(user).get();
tempList.push(snap.data().user)
});
With the first way, you are actually fetching the entire users collection and transmit all the corresponding data from the backend (Firestore) to your front-end. This is really not efficient, especially if you want to filter 50 users out 500k! Note also that you will pay for 500K reads instead of 50 (see pricing).
So fetching for only the docs you want (i.e. for exactly the 50 users) is the most efficient way. Since the get() method is asynchronous and returns a Promise, you can use Promise.all() as follows:
const matchingUsers = [user1, user2, user3, ..., user50];
const promises = matchingUsers.map(u => db.collection('users').doc(u).get());
Promise.all(promises).then(results => {
//results is an array of DocumentSnapshots
//use any array method, like map or forEach
results.map(docSnapshot => {
console.log(docSnapshot.data());
});
});
As explained in the doc, the advantage of Promise.all() is that "it returns a single Promise that fulfills when all of the promises passed as an iterable have been fulfilled", making it really easy to manage the different asynchronous parallel calls.

Removing nested api call from for loop

i made an api call to return a set of data(ex: users - list type, with the returned data i created a for loop and and within my for loop i make another api call to get the user's profile detail based on the user's id. I know this isn't the best practice and i was wondering how i could go about refactoring it.
api.get(...).then(response => {
this.users = response;
for(let i=0; i<this.users.length; i++){
api.get(...this.users[i].id).then(response => {
if(response.name == this.users[i].name)
this.newList.push(response);
})
}
})
and in my html i loop over this.newList to display the info that i need.
How can i remove the nested api call from within the for loop and still get the same results?
One possible solution is to use async/await. Although this will not remove nested loop, but make code look better
Example
async function getUsersAndProfiles () {
try {
this.users = await api.get(...);
for(let i=0; i<this.users.length; i++){
let response = await api.get(...this.users[i].id);
if (response.name == this.users[i].name)
this.newList.push(response);
}
}
catch (e)
console.log(e);
}
You can even move api call for user profile to another async function for possible future reuse and better code structure
Just push array of request into array after that we can use Promise.all() to make a request at a time, then we could create newList based on results array.
api.get(...).then(response => {
this.users = response;
const promises = this.users.map(user => api.get(...user.id))
const result = Promise.all(promises).then(result => {
this.newList = results.filter((user, i) => user.name === this.users[i].name)
})
})

Categories