Im beginner and trying to better understand API calls.
I want to make a call to a bible api that retrieves all books.
I then need to make a call to the same api with the book # to retrieve all chapters for requested book.
I then want to display a list of the books along with the chapters.
To this I made a utility function that loops through the books and returns the chapters. This is where it breaks.
I was able to retrieve the books and display them. But I am stuck as to how the make the second API call. I keep getting an error that I cannot serialize object Promise.
Also what is the best way to console log here in Next? Im not sure how to go about seeing what its being returned.
Here is what I have so far:
export default function Home(props) {
console.log(props);
return (
<div className="container">
<div>{/** display books & chapters */}</div>
</div>
);
}
export async function getStaticProps() {
// get all books
const reqBooks = await fetch(`https://getbible.net/v1/web/books.json`);
const resBooks = await reqBooks.json();
// convert to array of objs
const books = await Object.entries(resBooks);
const chapters = await getChapters(books);
return {
props: {
books,
chapters,
},
};
}
// utility... loop through books and make api calls to get chapters for each book
async function getChapters(books) {
const chaptersArr = [];
books.map((item, idx) => {
//
let url = item[1].url;
let bookChapters = fetch(url).then(response => response.json().then(data => data));
arr.push(bookChapters);
});
return chaptersArr;
}
The problem is that you're pushing promises into an array, not the values inside the promises. Instead of using that array, you can return in the map directly, and use Promise.all to get the values out. (You could use the array too, but there's no need for it since you're using a map). I lifted the getBooks call into its own function for clarity here, but the important change is what's going on in getChapters in the map:
async function getBooks () {
const res = await fetch('https://getbible.net/v1/web/books.json')
const json = await res.json()
return Object.entries(json)
}
async function getChapters (books) {
const chapters = await Promise.all(
books.map(async (item) => {
const url = item[1].url
const res = await fetch(url)
const json = await res.json()
return json
})
)
return chapters
}
export async function getStaticProps() {
const books = await getBooks()
const chapters = await getChapters(books)
return {
props: {
books,
chapters,
},
}
}
You can test this in plain Node (assuming node-fetch or a similar package) or the browser outside of Next with something like:
getStaticProps().then(data => {
console.log(JSON.stringify(data, null, 2))
})
Related
My app has a search bar where the user types in a word and clicks search. Upon click, the app fetches the definition of that word from an open dictionary API, and then updates the parent component's state to reflect the results. The component is then supposed to render the results by passing that as a prop to a presentational child component.
However, it looks like the state gets set before the fetch call has the time to return the data. So the search results are not rendered until the search button is clicked again, causing confusion.
I have tried resolving this by making my function asynchronous (Dictionary.search refers to an imported function which handles the fetch and returns the result in the form of an array):
async search(term) {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
}
However this only works some of the time. For longer searches, it doesn't wait before updating the state and re-rendering.
I have done some googling and came across this suggestion, but it also doesn't work:
search = (term) => {
const dictionarySearch = async (term) => {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
};
dictionarySearch(term);
};
EDITED TO ADD: Here is the Dictionary.search code, along with its helper function:
//Create an empty array in which the results of the API call will be stored
let results = [];
const Dictionary = {
search(term) {
//Url with our search term and API key:
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
//Fetch the results from Merriam Webster API:
fetch(url)
.then((response) => {
//Catch an error from their server
if (!response.ok) {
throw new Error("Network response was not ok");
}
//Return javaScript object version of the response
return response.json();
})
.then((jsonResponse) => {
//Perform the helper function on the javascript object and return the result (array)
return shortDef(jsonResponse);
})
//Catch any other errors than a server error
.catch((error) => {
console.error(
"There has been a problem with your fetch operation:",
error
);
});
//Create a copy of the results array
let returnResults = results.slice();
//Reset the original array to an empty array
results = [];
//Return the copy
return returnResults;
},
};
//Helper function to extract only certain info from the API
function shortDef(response) {
response.forEach((object) => {
//Create a new object for each object int he response array
let result = { word: "", type: "", definitions: [] };
//Add the word and type to the object
result.word = object.hwi.hw;
result.type = object.fl;
//Add the definitions to the object. There may be several, so it is pushed to an array.
let defs = object.shortdef;
defs.forEach((def) => {
result.definitions.push(def);
});
//Add the object to the array of API results
results.push(result);
});
//Return the list of results
return results;
}
I don't want to call the API in the ComponentDidMount, because it should get called every time the user presses "search". I also would prefer not to use useEffect, as it would mean refactoring my entire component from a class to a function.
Is there no way to have the setState in a class component wait for an asynchronous task to complete?
The problem is that your Dictionary.search function immediately returns, because it does not wait until the .then block resolves. Change it to an async function and await the fetch of the url. It should look like this:
const Dictionary = {
// Make search an async function
search: async term => {
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
// Await the results
await fetch(url)
.then(response => {
// ...
})
.then(jsonResponse => {
// ...
})
.catch(error => {
// ...
});
return;
},
};
I'm developing the front-end for my spring boot application. I set up an initial call wrapped in a useEffect() React.js function:
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
'/myapi/' + auth.authState.id
);
setData(data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
The data returned isn't comprehensive, and needs further call to retrieve other piece of information, for example this initial call return an employee id, but if I want to retrieve his name and display it I need a sub-sequential call, and here I'm experiencing tons of issues:
const getEmployeeName = async id => {
try {
const name = await fetchContext.authAxios.get(
'/employeeName/' + id
);
console.log((name["data"])); // <= Correctly display the name
return name["data"]; // return an [Object promise],
} catch (err) {
console.log(err);
}
};
I tried to wrap the return call inside a Promise.resolve() function, but didn't solve the problem. Upon reading to similar questions here on stackoverflow, most of the answers suggested to create a callback function or use the await keyword (as I've done), but unfortunately didn't solve the issue. I admit that this may not be the most elegant way to do it, as I'm still learning JS/React I'm open to suggestions on how to improve the api calls.
var output = Object.values(data).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Async functions always return promises. Any code that needs to interact with the value needs to either call .then on the promise, or be in an async function and await the promise.
In your case, you should just need to move your code into the existing useEffect, and setState when you're done. I'm assuming that the employeeID is part of the data returned by the first fetch:
const [name, setName] = useState('');
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
"/myapi/" + auth.authState.id
);
setData(data);
const name = await fetchContext.authAxios.get(
'/employeeName/' + data.employeeID
);
setName(name.data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
// ...
var output = Object.values(appointmentsData).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Note that the above code will do a rerender once it has the data (but no name), and another later when you have the name. If you want to wait until both fetches are complete, simply move the setData(data) down next to the setName
I'm using Next.js / React.js. I'm using this API to get a specific country.
There's an array on this response called borders, for example.
borders: [
"CAN",
"MEX",
],
There's an end point to get the data based on a border, for example.
https://restcountries.eu/rest/v2/alpha/can
How would I get the data both borders, i.e. each element in the borders array? It's two API calls that I've tried to make in a loop, but I get undefined.
export async function getServerSideProps(context) {
const { name } = context.params;
const res = await fetch(`https://restcountries.eu/rest/v2/name/${name}?fullText=true`)
const countryRes = await res.json();
const country = countryRes[0];
// Get the borders
const borders = country.borders;
// I'm making an API call to each element in array
const borderCountr = borders.forEach(border => {
fetch(`https://restcountries.eu/rest/v2/alpha/${border}`);
});
console.log(borderCountr); // undefinded
if (!country) {
return {
notFound: true,
}
}
return {
props: { country }
}
}
A good approach would be using Promise.all, to be sure that each fetch is correctly executed. Also, you need to make those calls async. something like:
const borderCountr = await Promise.all(
borders.map(async (border) => {
const response = await fetch(`https://restcountries.eu/rest/v2/alpha/${border}`);
return await response.json();
})
);
console.log(borderCountr[0], borderCountr[1]);
// I'm making an API call to each element in array
const borderCountr = borders.forEach(border => {
fetch(`https://restcountries.eu/rest/v2/alpha/${border}`);
});
This is not awaited (you do not await for ANY of the fetch results) that mens, the code executes here, and without waiting for the fetches to finish - executed the next lines.
As forEach returns undefined, that is your variable content.
This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
Closed 2 years ago.
I am creating an array of objects from an endpoint with the fetch api. Each object in the response contains a new url and I scan this array for new objects. The purpose is to access the endpoint, obtain the urls and access these urls to store your objects and attributes. But when accessing the array with objects and from an index it returns undefined.
let url = [];
let pokemon = [];
function getPokemons(){
fetch("https://pokeapi.co/api/v2/type/11/")
.then(async response =>{
await response.json().then(data =>
data.pokemon.forEach(item =>
url.push(item.pokemon.url)
)
)
}).then( response => {
url.map(item =>
fetch(item).then(response =>{
response.json().then(data =>
pokemon.push(data)
)
})
)
})
}
getPokemons()
console.log(pokemon[1])
Solution for web browser :
The node-fetch module is not required, I just clean your code using async/await syntax.
// getPokemon is now an async function for cleaner syntax
async function getPokemon() {
const pokemon = [];
// Then we can use await
const response = await fetch("https://pokeapi.co/api/v2/type/11/"),
json = await response.json(),
urls = json.pokemon.map(item => item.pokemon.url);
for (const url of urls) {
const response = await fetch(url),
json = await response.json();
pokemon.push(json)
}
// We return the pokemon array instead of polluting global scope
return pokemon;
};
getPokemon().then(pokemon => {
console.log(pokemon)
});
Hope it help !
NodeJS solution
You must install node-fetch (see npmjs.org :
npm install -D node-fetch
Then you can use a fetch:
// Importing the node module.
// You can delete this line if your not using a node environment.
const fetch = require('node-fetch');
// getPokemon is now an async function for cleaner syntax
async function getPokemon() {
const pokemon = [];
// Then we can use await
const response = await fetch("https://pokeapi.co/api/v2/type/11/"),
json = await response.json(),
urls = json.pokemon.map(item => item.pokemon.url);
for (const url of urls) {
const response = await fetch(url),
json = await response.json();
pokemon.push(json)
}
// We return the pokemon array instead of polluting global scope
return pokemon;
};
getPokemon().then(pokemon => {
console.log(pokemon)
});
Explanation
fetch is not part of the Javascript language specification but is a Web API. Each browser may or not choose to implement it. Each Implementations may work differently under the hood but the Javascript API provided must match the standard (MDN Web Docs).
This is why you need a module to fetch the data.
EDIT : Adding solution for web browser environment
The problem is that the function getPokemons should be async.
You should await it before accessing:
getPokemons().then(()=> console.log(pokemon[1]))
// or if it inside an async function:
await getPokemons();
console.log(pokemon[1]);
But there's another reason also. You have internal promises outside the parent promise chain. I mean:
.then((response) => {
// this is array of promises
// you should return it to have ability await it on parent promise
url.map((item) =>
fetch(item).then((response) => {
response.json().then((data) => pokemon.push(data));
})
);
});
Your code might look like:
// if you really need global variable
let pokemon = [];
async function getPokemons() {
const result = await fetch("https://pokeapi.co/api/v2/type/11/")
.then((response) => response.json())
.then((data) =>
Promise.all(
data.pokemon.map((item) =>
fetch(item.pokemon.url).then((response) => response.json())
)
)
);
pokemon.push(...result);
}
getPokemons().then(() => {
console.log(pokemon[1]);
});
Or, the same result without global variables:
function getPokemons() {
return fetch("https://pokeapi.co/api/v2/type/11/")
.then(...)
.then(...);
}
getPokemons().then((pokemons) => {
console.log(pokemons[1]);
});
I'm trying to do an API call using fetch().
I'm aware that fetch() returns a Promise and should be handled using .then or await. The same for the result.json() Followed this tutorial http://www.reactnativeexpress.com/networking, I arrived with fetchRoute()function. The console.log(route) inside the function is never called.
I tried to return console.log(fetchRoute(this.state.userLocation, text)), but it was still returning a Promise.
I read another quesiton here on Stack Overflow (sorry, can't find the link anymore), and they said to try something like this:
getRouteHandler = (text) => {
fetchRoute(this.state.userLocation, text).then(json => console.log(json));
Still, I couldn't log the fetch results. Anyone knows what could be going wrong? Here is the relevant code:
const fetchRoute = async (ori, dest) => {
let origin = ori.latitude+','+ori.longitude;
let destination = encodeURIComponent(dest);
const key = "MyAPIKey";
const URL = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin}&destination=${destination}&key=${key}`;
try{
const response = await fetch(URL)
const route = await response.json()
console.log(route)
return route
}catch(e){
return e
}
}
export default class App extends Component{
state = {
userLocation: null,
route: [],
}
getRouteHandler = (text) => {
fetchRoute(this.state.userLocation, text).then(json => console.log(json));
}
Sometimes if you're fetching large amounts of data it will take awhile for it to log. For example, in a past project the api I was fetching from had close to 5 million records. It took a few minutes to see anything in the console.