Problem with React Query's Infinite Query, using Edamam API - javascript

I currently have some issues trying to add the infinite query feature to a recipes app I'm working on using Edamam API.
All the examples I have looked for (even React Query's documentation) implement the infinite scroll using a page/cursor number system... I understand this is the ideal way, but... Edamam API doesn't work this way with paginated queries.
Instead, the API has the following structure for each recipe query we look for (let's assume we are searching for "chicken", this would be the JSON structure):
from: 1,
to: 20,
count: 10000,
_links: {
next: {
href: "https://api.edamam.com/api/recipes/v2?q=chicken&app_key=APIKEYc&_cont=CHcVQBtNNQphDmgVQntAEX4BYldtBAAGRmxGC2ERYVJ2BwoVX3cVBWQSY1EhBQcGEmNHVmMTYFEgDQQCFTNJBGQUMQZxVhFqX3cWQT1OcV9xBB8VADQWVhFCPwoxXVZEITQeVDcBaR4-SQ%3D%3D&type=public&app_id=APPID"
title: "Next Page"
}
},
hits: [{}] ... (This is where the actual recipes are)
As you can see, there is no numbering system for paginated queries, instead, it's a whole URL and it's giving me a hard time since I'm also new to React Query.
I tried the following, but it just fetches the same data over and over again as I reach the bottom of the page:
const getRecipes = async ({ pageParam }) => {
try {
const path = pageParam
? pageParam
: `https://api.edamam.com/api/recipes/v2?q=${query}&app_id=${process.env.REACT_APP_APP_ID}&app_key=${process.env.REACT_APP_API_KEY}&type=public`;
const response = await axios.get(path);
return response.data;
} catch (error) {
console.log(error);
}
const { ref, inView } = useInView();
useEffect(() => {
inView && fetchNextPage();
}, [inView]);
const {
data,
isFetching,
isFetchingNextPage,
error,
status,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery(
["recipes", query],
({ pageParam = "" }) => getRecipes(pageParam),
{
getNextPageParam: (lastPage) => lastPage._links.next.href,
}
);
Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
Please help!

Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
I'd say that this is the correct approach. The only code issue I can see in your example is that you destruct page param, and then pass the page param string to getRecipes:
({ pageParam = "" }) => getRecipes(pageParam),
but in getRecipes, you expect an object to come in (which you again destructure):
const getRecipes = async ({ pageParam }) => {
You can fix that by either changing the call side, or the function syntax, and then it should work.

Related

How can I make API calls conditionally using Redux Toolkit query/createApi?

I am only using RTK to make API calls. Not using for any state management stuff like using Slices or Redux Thunk etc. I don't have a clear understanding of those yet... (just a full disclosure)
I have tried this:
export const gymApi = createApi({
reducerPath: 'gymApi',
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getAllWorkouts: builder.query({
query: () => makeApiCall(`/exercises`),
}),
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return null;
}
},
})
});
And tried this:
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return Promise.resolve({ data: [] }); // or Promise.resolve({});
}
},
})
with no luck. I'm calling the hooks from the home page like this:
const Exercises = ({ exercises, setExercises, bodyPart }) => {
// THE FOLLOWING WORKS. ONLY USGING STATIC DATA DUE TO API HARD LIMITS
const { data: exercisesData, isFetching: isFetchingAllWorkouts } =
useGetAllWorkoutsQuery();
const { data: exercisesDataByCategory, isFetching: isFetchingByCategory } =
useGetWorkoutByBodyPartQuery(bodyPart);
useEffect(() => {
if (exercisesData && exercisesDataByCategory) {
if (bodyPart === 'all') {
setExercises(exercisesData);
} else {
setExercises(exercisesDataByCategory);
}
}
}, [exercisesData, bodyPart, isFetchingAllWorkouts, isFetchingByCategory]);
It is working but with every refresh, I get a Warning: "Category not found ..." from the API ... basically, there is no endpoint called "all" in the ExerciseDB API (in RapidAPIs). So every time an "all" is passed as a Category, it gives me a 401. Now the App works fine. I was just wondering if there is a cleaner way to do this. I mean, I don't wanna make a call to the API when the Category is "all".
I must say that I'm new to this. Trying to get out of using Fetch all the time and take advantage of RTK caching. Any help in the correct direction will be highly appreciated. Thanx in advance.
You can either pass {skip: true} as an option to the query hook, or import the special skipToken value from RTK and pass that as the query argument.
See the RTK docs for more details:
https://redux-toolkit.js.org/rtk-query/usage/conditional-fetching

React & Axios - Not receiving response data upon page refresh

Working on a project here and ran into an issue. I haven't had this problem before but now I do for some reason.
So I am making an GET request to ASOS API, but it is acting very strange. Some of these, such as name is received upon page refresh, but mainly the other things like information about the brand becomes undefined. Now, the brand is in another object inside of the API. But I have had other things at other parts of the page before that were also in objects. But I did not have any issue there.
Here is how the API call looks like:
And here is my code for the API fetch:
const FetchAPI = (props) => {
const [product, setProduct] = useState({});
const [brand, setBrand] = useState({});
const [price, setPrice] = useState({});
const [params, setParams] = useState({
id: "23363645",
lang: "en-US",
store: "US",
sizeSchema: "US",
currency: "USD",
});
useEffect(() => {
const options = {
method: "GET",
url: "https://asos2.p.rapidapi.com/products/v3/detail",
params: params,
headers: {
"x-rapidapi-key": "",
"x-rapidapi-host": "",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data);
setProduct(response.data);
setBrand(response.data.brand);
setPrice(response.data.price.current);
})
.catch(function (error) {
console.error(error);
});
}, []);
return (
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description.replace( /(<([^>]+)>)/ig, '')}
/>
</div>
);
};
I am sending the data over to Product which is the product page of the item I am requesting. But whenever I refresh the page, I get TypeError: Cannot read property 'replace' of undefined. I did remove replace, and it worked fine. And if I placed replace back into the brand.description and saved, still worked fine. But on the page refresh, it crashes.
Is it perhaps trying to load my return before the useEffect? If so, how do I solve this?
I think the issue here is that while data from API is being fetched, brand.description is undefined and there is no replace method on undefined. You can either do this - >
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description ? brand.description.replace( /(<([^>]+)>)/ig, '') : ""}
/>
</div>
or
const [brand, setBrand] = useState({ description: ""});
and keep the remaining code same.
First of all, unrelated to your question, you have many superfluous state variables. You have product which stores all the data of the product and then price and brand which stores subsets of the same data.
Consider using only the product state variable, or if you want to keep the names do something like
const price = product.price.current;
Second, your default value for brand is an empty object, meaning brand.description is undefined.
You can solve this with optional chaining like so:
<Product
name={product?.name}
price={price?.text}
brand={brand?.description?.replace( /(<([^>]+)>)/ig, '')}
/>
The useEffect hook gets called once the component is rendered so in this case initially when your API is not called your brand.description will be undefined and when you are trying to use replace on undefined the error is coming. So you can always add a check using optional chaining(?.) so even if we don't get the description in the brand it will not break the website and you should also use a loader till the API call is through.
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description?.replace( /(<([^>]+)>)/ig, '')}
/>
</div>
useEffect gets called only after the component is rendered . So when the component is rendered for the first time you have the state brand as an empty object . So what you are trying to do is {}.description -> . This is undefined .
This is the reason why its a good practice to always have a loading state when the component is making an api call.
const [ loading, setLoading ] = useState(true);
const getProductDetails = async() => {
const options = {
method: "GET",
url: "https://asos2.p.rapidapi.com/products/v3/detail",
params: params,
headers: {
"x-rapidapi-key": "",
"x-rapidapi-host": "",
},
};
try {
const { data } = await axios.request(options);
setProduct(data);
setBrand(data.brand);
setPrice(data.price.current);
}
catch(error){
console.log(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
getProductDetails();
}, []);
if(loading)
return <p> Loading ... </p>
return (
// return your JSX here
)

Skipping top level of JSON data and retrieving data below it via JavaScript

Via a microservice, I retrieve several packages of JSON data and spit them out onto a Vue.js-driven page. The data looks something like this:
{"data":{"getcompanies":
[
{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}
]
}}
{"data":{"getusers":
[{"id":22,"name":"Fred","address":"Parmesean Street","zip":"15090"},
{"id":24,"name":"George","address":"Loopy Lane","zip":"15090"},
{"id":25,"name":"Lucy","address":"Farm Road","zip":"15090"}]}}
{"data":{"getdevices":
[{"id":2,"name":"device type 1"},
{"id":4,"name":"device type 2"},
{"id":5,"name":"device type 3"}]}}
...and I successfully grab them individually via code like this:
getCompanies() {
this.sendMicroServiceRequest({
method: 'GET',
url: `api/authenticated/function/getcompanies`
})
.then((response) => {
if(response.data) {
this.dataCompanies = response.data.getcompanies
} else {
console.error(response)
}
}).catch(console.error)
}
...with getUsers() and getDevices() looking respectively the same. getCompanies() returns:
[{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}]
...which I relay to the Vue template in a table, and this works just fine and dandy.
But this is obviously going to get unwieldy if I need to add more microservice calls down the road.
What I'm looking for is an elegant way to jump past the response.data.*whatever* and get to those id-records with a re-useable call, but I'm having trouble getting there. response.data[0] doesn't work, and mapping down to the stuff I need either comes back undefined, or in bits of array. And filtering for response.data[0].id to return just the rows with ids keeps coming back undefined.
My last attempt (see below) to access the data does work, but looks like it comes back as individual array elements. I'd rather not - if possible - rebuild an array into a JSON structure. I keep thinking I should be able to just step past the next level regardless of what it's called, and grab whatever is there in one chunk, as if I read response.data.getcompanies directly, but not caring what 'getcompanies' is, or needing to reference it by name:
// the call
this.dataCompanies = this.getFullData('companies')
getFullData(who) {
this.sendMicroServiceRequest({
method: 'GET',
url: 'api/authenticated/function/get' + who,
})
.then((response) => {
if(response) {
// attempt 1 to get chunk below 'getcompanies'
Object.keys(response.data).forEach(function(prop) {
console.log(response.data[prop])
})
// attempt 2
// for (const prop in response.data) {
// console.log(response.data[prop])
// }
let output = response.data[prop] // erroneously thinking this is in one object
return output
} else {
console.error(response)
}
}).catch(console.error)
}
...outputs:
(63) [{…}, {…}, {…}] <-- *there are 63 of these records, I'm just showing the first few...*
0: {"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"}
1: {"id":7,"name":"McMillan","address":null,"zip":"15090"},
2: {"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}...
Oh, and the return above comes back 'undefined' for some reason that eludes me at 3AM. >.<
It's one of those things where I think I am close, but not quite. Any tips, hints, or pokes in the right direction are greatly appreciated.
I feel it's better to be explicit about accessing the object. Seems like the object key is consistent with the name of the microservice function? If so:
getData(functionName) {
return this.sendMicroServiceRequest({
method: 'GET',
url: "api/authenticated/function/" + functionName
})
.then( response => response.data[functionName] )
}
getCompanies(){
this.getData("getcompanies").then(companies => {
this.dataCompanies = companies
})
}
let arrResponse = {data: ['x']};
let objResponse = {data: {getcompanies: 'x'}};
console.log(arrResponse.data[0]);
console.log(Object.values(objResponse.data)[0]);
response.data[0] would work if data was an array. To get the first-and-only element of an object, use Object.values(response.data)[0] instead. Object.values converts an object to an array of its values.
Its counterparts Object.keys and Object.entries likewise return arrays of keys and key-value tuples respectively.
Note, order isn't guaranteed in objects, so this is only predictable in your situation because data has exactly a single key & value. Otherwise, you'd have to iterate the entry tuples and search for the desired entry.
firstValue
Let's begin with a generic function, firstValue. It will get the first value of an object, if present, otherwise it will throw an error -
const x = { something: "foo" }
const y = {}
const firstValue = t =>
{ const v = Object.values(t)
if (v.length)
return v[0]
else
throw Error("empty data")
}
console.log(firstValue(x)) // "foo"
console.log(firstValue(y)) // Error: empty data
getData
Now write a generic getData. We chain our firstValue function on the end, and be careful not to add a console.log or .catch here; that is a choice for the caller to decide -
getData(url) {
return this
.sendMicroServiceRequest({ method: "GET", url })
.then(response => {
if (response.data)
return response.data
else
return Promise.reject(response)
})
.then(firstValue)
}
Now we write getCompanies, getUsers, etc -
getCompanies() {
return getData("api/authenticated/function/getcompanies")
}
getUsers() {
return getData("api/authenticated/function/getusers")
}
//...
async and await
Maybe you could spruce up getData with async and await -
async getData(url) {
const response =
await this.sendMicroServiceRequest({ method: "GET", url })
return response.data
? firstValue(response.data)
: Promise.reject(response)
}
power of generics demonstrated
We might even suggest that these get* functions are no longer needed -
async getAll() {
return {
companies:
await getData("api/authenticated/function/getcompanies"),
users:
await getData("api/authenticated/function/getusers"),
devices:
await getData("api/authenticated/function/getdevices"),
// ...
}
}
Above we used three await getData(...) requests which happen in serial order. Perhaps you want all of these requests to run in parallel. Below we will show how to do that -
async getAll() {
const requests = [
getData("api/authenticated/function/getcompanies"),
getData("api/authenticated/function/getusers"),
getData("api/authenticated/function/getdevices")
]
const [companies, users, devices] = Promise.all(requests)
return { companies, users, devices }
}
error handling
Finally, error handling is reserved for the caller and should not be attempted within our generic functions -
this.getAll()
.then(data => this.render(data)) // some Vue template
.catch(console.error)

React: Realtime rendering Axios return

hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.

In rxjs, how do I chain mapping through arrays of data received from different API's?

I'm calling an API and receiving an array of results, I'm checking for pagination and if more pages exist I call the next page, repeat until no more pages.
For each array of results, I call another endpoint and do the exact same thing: I receive an array of results, check for another page and call endpoint again. Wash, rinse repeat.
For instance:
I want to grab a list of countries that might be a paginated response, then for each country I want to grab a list of cities, which might also be paginated. And for each city I execute a set of transformations and then store in a database.
I already tried this, but got stuck:
const grabCountries = Observable.create(async (observer) => {
const url = 'http://api.com/countries'
let cursor = url
do {
const results = fetch(cursor)
// results = {
// data: [ 'Canada', 'France', 'Spain' ],
// next: '47asd8f76358df8f4058898fd8fab'
// }
results.data.forEach(country => { observer.next(country) })
cursor = results.next ? `${url}/${results.next}` : undefined
} while(cursor)
})
const getCities = {
next: (country) => {
const url = 'http://api.com/cities'
let cursor = url
do {
const results = fetch(cursor)
// results = {
// data: [
// 'Montreal', 'Toronto',
// 'Paris', 'Marseilles',
// 'Barcelona', 'Madrid'
// ],
// next: '89ghjg98nd8g8sdfg98gs9h868hfoig'
// }
results.data.forEach(city => {
`**** What do I do here?? ****`
})
cursor = results.next ? `${url}/${results.next}` : undefined
} while(cursor)
}
}
I tried a few approaches:
Making a subject (sometimes I'll need to do parallel processed base on the results of 'grabCountries'. For example I may want to store the countries in a DB in parallel with grabbing the Cities.)
const intermediateSubject = new Subject()
intermediateSubject.subscribe(storeCountriesInDatabase)
intermediateSubject.subscribe(getCities)
I also tried piping and mapping, but it seems like it's basically the same thing.
As I was writing this I thought of this solution and it seems to be working fine, I would just like to know if I'm making this too complicated. There might be cases where I need to make more that just a few API calls in a row. (Imagine, Countries => States => Cities => Bakeries => Reviews => Comments => Replies) So this weird mapping over another observer callback pattern might get nasty.
So this is what I have now basically:
// grabCountries stays the same as above, but the rest is as follows:
const grabCities = (country) =>
Observable.create(async (observer) => {
const url = `http://api.com/${country}/cities`
let cursor = url
do {
const results = fetch(cursor)
// results = {
// data: [
// 'Montreal', 'Toronto',
// 'Paris', 'Marseilles',
// 'Barcelona', 'Madrid'
// ],
// next: '89ghjg98nd8g8sdfg98gs9h868hfoig'
// }
results.data.forEach(city => {
observer.next(city)
})
cursor = results.next ? `${url}/${results.next}` : undefined
} while (cursor)
})
const multiCaster = new Subject()
grabCountries.subscribe(multiCaster)
multiCaster.pipe(map((country) => {
grabCities(country).pipe(map(saveCityToDB)).subscribe()
})).subscribe()
multiCaster.pipe(map(saveCountryToDB)).subscribe()
tl;dr - I call an API that receives a paginated set of results in an array and I need to map through each item and call another api that receives another paginated set of results, each set also in an array.
Is nesting one observable inside another and mapping through the results via 'callApiForCountries.pipe(map(forEachCountryCallApiForCities))' the best method or do you have any other recommendations?
Here's the code that should work with sequential crawling of next url.
You start with a {next:url} until res.next is not available.
of({next:http://api.com/cities}).pipe(
expand(res=>results.next ? `${url}/${results.next}` : undefined
takeWhile(res=>res.next!==undefined)
).subscribe()
OK, so I have spent a lot of brain power on this and have come up with two solutions that seem to be working.
const nestedFlow = () => {
fetchAccountIDs.pipe(map(accountIds => {
getAccountPostIDs(accountIds) // Has the do loop for paging inside
.pipe(
map(fetchPostDetails),
map(mapToDBFormat),
map(storeInDB)
).subscribe()
})).subscribe()
}
const expandedflow = () => {
fetchAccountIDs.subscribe((accountId) => {
// accountId { accountId: '345367geg55sy'}
getAccountPostIDs(accountId).pipe(
expand((results) => {
/*
results : {
postIDs: [
131424234,
247345345,
],
cursor: '374fg8v0ggfgt94',
}
*/
const { postIDs, cursor } = results
if (cursor) return getAccountPostIDs({...accountId, cursor})
return { postIDs, cursor }
}),
takeWhile(hasCursor, true), // recurs until cursor is undefined
concatMap(data => data.postIDs),
map(data => ({ post_id: data })),
map(fetchPostDetails),
map(mapToDBFormat),
map(storeInDB)
).subscribe()
})
}
Both seem to be working with similar performance. I read some where that leaving the data flow is a bad practice and you should pipe everything, but I don't know how to eliminate the first exit in the 'expandedFlow' because the 'expand' needs to call back an observable, but maybe it can be done.
Now I just have to solve the race condition issues from the time the 'complete' is called in getAccountPostIDs the the last record is stored in the DB. Currently in my test, the observer.complete is finishing before 3 of the upsert actions.
Any comments are appreciated and I hope this helps someone out in the future.
What you need is the expand operator. It behaves recursively so it fits the idea of having paginated results.

Categories

Resources