map redux saga results - arrays

Code
I followed this stackoverflow post to understand how to use the map with the yield.
my code is splitted in 3 parts:
Example data
citiesCode = [
{
bankCityId: A305
cityId: B544
},
{
bankCityId: R394
cityId: D873
},
]
1) the function that invoke when i launch the relative action
export function* getInvoiceCities({ citiesCode }) {
yield call(invoiceCities, citiesCode);
}
2)this function allow me to map the array that citiesCode is
export function* invoiceCities(citiesCode) {
yield all(citiesCode.map(cityCode => call(getCityInfo, cityCode)));
}
3) in this last function i use the relative code to made a call to the bankCityUrl and cityUrl to obtain the information about the relative city.
const citiesInfoList = [];
function* getCityInfo({ bankCity, city }) {
const cityUrl = `/cities/${city}`;
const bankCityUrl = `/cities/${bankCity}`;
try {
const cityInfoResponse = yield call(fetchWrapper, {
url: cityUrl,
});
const bankCityInfoResponse = yield call(fetchWrapper, {
url: bankCityUrl,
});
citiesInfoList.push(cityInfoResponse, bankCityInfoResponse);
console.log('cities.info', citiesInfoList);
// if (cityInfoResponse.status && bankCityInfoResponse.status === 'success') {
// yield put(saveInvoiceCitiesResponse(citiesInfoList));
// }
} catch ({ packet, response }) {
if (response.status !== 422) {
yield put(pushError({ text: 'sendPraticeSectionError' }));
}
}
BUG
The main bug is: If I call multiple time getInvoiceCities save to make this redux call I store the same cities more and more time.
Just to make an example:
citiesInfoList = []
I call it for the first time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results
I call it for the second time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results x 2
I call it for the second time: I console.log('cities.info', citiesInfoList); citiesInfoList will be filled with the right results x 3
there is a way to avoid this behaviour ? can i stop to store multiple times the same results ?

This scenario is happening because all your sagas sits in one file
If I undertood you correctly, when you call an action that invokes getInvoiceCities, it will invoke invoiceCities, and the later will invoke getCityInfo for each cityCode.
BUT, since you have defined const citiesInfoList = []; in the same module. What happens next is that getCityInfo will start using citiesInfoList for each getCityInfo invocation, resulting in a duplicated results as you have described.
I suggest the following:
Recommended: Get citiesInfoList out of this file to a separate file, and build a reducer to manage it.
Reset citiesInfoList = [] before every push.

i found, just some hours ago, an easy answer.
1) i deleted citiesInfoList = [] ( it was very ugly to see imho)
2) in the second function i did this
export function* invoiceCities(citiesCode) {
const results = yield all(
citiesCode.map(cityCode => call(getCityInfo, cityCode)),
);
yield put(saveInvoiceCitiesResponse(results));
}

Related

Velo by Wix, JSON data in repeater

I'm trying to get the temperature of each hour from this website: https://www.smhi.se/vader/prognoser/ortsprognoser/q/Stockholm/2673730
I'm getting the data from https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json. The "t" object is the temperature.
The problem I have is displaying the data for each hour in the repeater.
Here is my backend-code:
import { getJSON } from 'wix-fetch';
export async function getWeather() {
try {
const response = await getJSON('https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json');
console.log(response) // all data
const tempData = response.timeSeries[0].parameters[10].values[0];
return tempData // Only returns "t" - temperature
} catch (e) {
return e;
}
}
The backend part works, however the frontend doesn't.
import { getWeather } from 'backend/getSMHI.jsw'
$w.onReady(function () {
(
getWeather().then(weatherInfo => {
$w('#weatherRepeater').onItemReady(($item, itemData, index) => {
if (index > 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[1].values[0];
} else if (index === 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[0].values[0];
} else {
$item('#tempText').text = itemData.timeSeries[index].parameters[10].values[0];
} // The parameters number for "t" changes depending on the index
})
$w('#weatherRepeater').data = weatherInfo;
})
)
})
Seems like there are at least a couple of issues here.
First, you are retrieving a single number from the API and trying to put that in a repeater. From the description of what you're trying to do, it would seem that you mean to be retrieving a list of numbers, probably as an array. You probably want to do some filtering and/or mapping on the response data instead of directly accessing a single value.
Second, the data you send to a repeater must be in the proper format. Namely, it must be an array of objects, where each object has a unique _id property value (as a string). You are not doing that here. You are simply assigning it a number.
Third, and this is just an efficiency thing, you don't need to define the onItemReady inside the then(). Not that it will really make much of a difference here.

React useEffect loop

I am implementing a React shopping cart, and I am having a locked loop problem with an useEffect hook.
Initially, I use an useEffect to fetch id's and quantities of products in the cart and store these values in a state variable array of objects named shopCart:
// fetches the id's and quantities of all products in the user's shopping cart
// and assigns these values to the shopCart array
// Each value of the shopCart array should be an object like this: {id:random_id, quantity:number_of_products }]
useEffect(() => {
let auxCart = []; // auxiliary array that will receive the result of the fetch request
// postData: performs a POST request with {userId:userId} as the body of the request
postData('REQUEST1_URL', { userId: userId })
.then(data => {
for (let i = 0; i < data.shopCart.length; i++) {
auxCart[i] = {};
auxCart[i].id = data.shopCart[i].id; //this shopCart is not the same shopCart state variable. Just a value received from the database
auxCart[i].quantity = data.shopCart[i].quantity;
}
});
setShopCart(auxCart);
}, [])
After that, I modify shopCart by adding attributes/keys with info about the products, such as product title, price and images:
// Modifies shopCart, which should contain only id's and quantities at the moment,
// assigning details to each product such as price and title.
// Each value of shopCart should be an object like this:
// {id:random_id, quantity:number_of_products,
// title:product_name, price:product_price,
// oldPrice:old_product_price, image:array_of_images,
// type:type_of_product}
useEffect(() => {
console.log('useEffect2');
if (!isFirstRender) { //Ref hook variable used to test if the useEffect is in the first render or not
let aux = [...shopCart]; // creates a copy of shopCart
for (let i = 0; i < shopCart.length; i++) {
// fetches the details of each product
fetch('REQUEST2_URL').
then(res => res.json()).
then(res => {
aux[i].image = res[0].image;
aux[i].title = res[0].title;
aux[i].price = res[0].price;
aux[i].oldPrice = res[0].oldPrice;
aux[i].type = res[0].type;
});
}
setShopCart(aux);
}
}, [shopCart]);
The problem is that the second useEffect keeps printing 'useEffect2' multiple times, showing that it is in a locked loop. How can I fix that ? I was expecting that, since the fetched products info doesn't change, the second useEffect would not be repeating everytime, but it does. I use shopCart in its dependecies arrays to wait for the first request to finish updating shopCart.
That is happening because inside your second useEffect, you have shopCart in your dependency array and inside the function you set shopCart there, hence it runs infinitely.
You could do all this use case inside the first useEffect. How? You could await for the first fetch to respond and then you for the second request. (You could keep then() if you want, but IMO is much cleaner async...await way)
Leaving it like:
useEffect(async () => {
let auxCart = []; // auxiliary array that will receive the result of the fetch request
// postData: performs a POST request with {userId:userId} as the body of the request
const data = await postData('REQUEST1_URL', { userId: userId })
for (let i = 0; i < data.shopCart.length; i++) {
auxCart[i] = {};
auxCart[i].id = data.shopCart[i].id; //this shopCart is not the same shopCart state variable. Just a value received from the database
auxCart[i].quantity = data.shopCart[i].quantity;
}
let aux = [...shopCart]; // creates a copy of shopCart
for (let i = 0; i < shopCart.length; i++) {
// fetches the details of each product
const res = await fetch('REQUEST2_URL').
const resData = await res.json();
aux[i].image = resData[0].image;
aux[i].title = resData[0].title;
aux[i].price = resData[0].price;
aux[i].oldPrice = resData[0].oldPrice;
aux[i].type = resData[0].type;
}
// do whatever you want with those objects
}, [])
To make the code much cleaner you could also get rid of those aux variables (seems much like imperative programming languages) and start using array functions like map, filter, etc. depending your need. Indeed in the first for loop you are just setting the same values to another array when you could directly use that array:
useEffect(async () => {
const shopCart = await postData('REQUEST1_URL', { userId: userId }).shopCart
let aux = [...shopCart]; // creates a copy of shopCart
for (let i = 0; i < shopCart.length; i++) {
// fetches the details of each product
const res = await fetch('REQUEST2_URL').
const resData = await res.json();
aux[i].image = resData[0].image;
aux[i].title = resData[0].title;
aux[i].price = resData[0].price;
aux[i].oldPrice = resData[0].oldPrice;
aux[i].type = resData[0].type;
}
// do whatever you want with those objects
}, [])
And with the second for..loop you could also use a map but it's a little more complicated since you should use an async func inside the map and wait for all Promises to resolve using Promise.all() and it's just a whole new solution... Leaving it for you ;)
There are 2 things creating the infinite loop in your code. With these 2 things combined, it creates the loop.
let aux = [...shopCart]; // creates a copy of shopCart
<= This action create a plain new object, therefore a new shopCart is always created, making React think it's a different object, even when the content might be the same
setShopCart(aux);
}
}, [shopCart]);
<= You are setting this new object to shopCart, and as React always think this is a new object, it always rerun the useEffect
How to fix this?
I'm not sure why you split into 2 different useEffect and adding the shopCart as dependency in the 2nd useEffect, but based on what you describe, you can always combine them into one useEffect, and use Promise or async/await to make sure the 2nd API call is only run after the 1st API call finishes. For more detail on this, I think Agustin posted a pretty good example one.

How to make an infinite scroll with react-query?

I'm trying to use react-query useInfiniteScroll with a basic API, such as the cocktaildb or pokeapi.
useInfiniteQuery takes two parameters: a unique key for the cache and a function it has to run.
It returns a data object, and also a fetchMore function. If fetchMore is called - through an intersection observer for exemple -, useInfiniteQuery call its parameter function again, but with an updated payload thanks to a native callback getFetchMore().
In the official documentation, getFetchMore automatically takes two argument: the last value returned, and all the values returned.
Based on this, their demo takes the value of the previous page number sent by getFetchMore, and performs a new call with an updated page number.
But how can I perform the same kind of thing with a basic api that only return a json?
Here is the official demo code:
function Projects() {
const fetchProjects = (key, cursor = 0) =>
fetch('/api/projects?cursor=' + cursor)
const {
status,
data,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery('projects', fetchProjects, {
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
})
infinite scrolling relies on pagination, so to utilize this component, you'd need to somehow track what page you are on, and if there are more pages. If you're working with a list of elements, you could check to see if less elements where returned in your last query. For example, if you get 5 new items on each new fetch, and on the last fetch you got only 4, you've probably reached the edge of the list.
so in that case you'd check if lastGroup.length < 5, and if that returns true, return false (stop fetching more pages).
In case there are more pages to fetch, you'd need to return the number of the current page from getFetchMore, so that the query uses it as a parameter. One way of measuring what page you might be on would be to count how many array exist inside the data object, since infiniteQuery places each new page into a separate array inside data. so if the length of data array is 1, it means you have fetched only page 1, in which case you'd want to return the number 2.
final result:
getFetchMore: (lastGroup, allGroups) => {
const morePagesExist = lastGroup?.length === 5
if (!morePagesExist) return false;
return allGroups.length+1
}
now you just need to use getMore to fetch more pages.
The steps are:
Waiting for useInfiniteQuery to request the first group of data by default.
Returning the information for the next query in getNextPageParam.
Calling fetchNextPage function.
Reference https://react-query.tanstack.com/guides/infinite-queries
Example 1 with rest api
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage) => {
// lastPage signature depends on your api respond, below is a pseudocode
if (lastPage.hasNextPage) {
return lastPage.nextCursor;
}
return undefined;
},
})
Example 2 with graphql query (pseudocode)
const {
data,
fetchNextPage,
isLoading,
} = useInfiniteQuery(
['GetProjectsKeyQuery'],
async ({ pageParam }) => {
return graphqlClient.request(GetProjectsQuery, {
isPublic: true, // some condition/variables if you have
first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
cursor: pageParam,
});
},
{
getNextPageParam: (lastPage) => {
// pseudocode, lastPage depends on your api respond
if (lastPage.projects.pageInfo.hasNextPage) {
return lastPage.projects.pageInfo.endCursor;
}
return undefined;
},
},
);
react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:
const projects = data.pages.flatMap((p) => p.projects.nodes)
Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:
const handleEndReached = () => {
fetchNextPage();
};
Graphql example query:
add to your query after: cursor:
query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
projects(
condition: {isPublic: $isPublic}
first: $first
after: $cursor
) ...

Superagent Not Returning Value From Then

superagent.get(URL).then((res) => {
for(let i in res.body) {
if (i==='has_rejected_advisories') {
console.log(i + "="+res.body[i]);
}
}
})
.catch((err) => err.message));
My result is:
has_rejected_advisories=false
But I am not able to use res.body[i] outside this function, i.e I want superagent function to return this value in a boolean variable to use it elsewhere.
ex.
a = superagent.get(URL).then((res) => {
for(let i in res.body) {
if(i==='has_rejected_advisories') {
console.log(i + "="+res.body[i]);
}
}
})
.catch((err) => err.message));
if(a===false){/*do this*/}
This is because the superagent.get(url) call is asynchronous. The value given to a is a Promise
Since this is async, the if (a === false) is actually executing before the function body passed to .then. You will either need to move this logic to the .then function, or use something like async/await if you like the synchronous looking syntax.
On top of jerelmiller's great advice you need to note the following:
Try this:
create a global var assuming it's a string
var mysares = ""
This example will only bring back 1 string back of everything!! Not single element. Also if you can't get the standard Fetch() to work don't try other methods like axios or superagents. Now use our global like so:
superagent.get(URL).then((res) => {
for(let i in res.body) {
if (i==='has_rejected_advisories') {
//Add comments as will help you
//to explain to yourself and others
//what you're trying to do
//That said if this iteration produces
//correct data then you're fine
//push my result as a string
mysares = res.body[i];
//infact what's in row 1?
mysares = res.body[0];
//Actually I code my own JSON!!
mysares = res.body[1];
console.log(i + "="+mysares);
}
}
})
.catch((err) => err.message));
Now you can do whatever:
if(mysares===false){/*do this*/
alert(playDuckHunt());}
Things to note:
res.body[i] is an iteration
You cannot use it outside of the function
Because:
It's local to that function
You don't know what position of 'i' is even if you could use it as you will be outside of your loop
One last thing:
Loops loop through loops or arrays etc.
So (in real world) you can't just request the value of the loop
unless you agree the position of data to be released,
type,and bucket (where it's going to be displayed or not).
Hope this helps!
PS> we need to know where 'has_rejected_advisories' is in the JSON so send us your json url as it must be a column/obj header name. Or it's any old 'a' then var a can be your "false"
In constructor:
this.state = {a:null};
In some function:
superagent.get(URL).then(
(res) => {for(let i in res.body)
{
if(i === 'has_rejected_advisories')
{
this.setState({a:res.body[i]})
}
}
}).catch((err)=>(err.message));
In render:
console.log(this.state.a);
Inside then() the value could be used using state variable but there are many scenarios we could not use them, like if we want to perform all the operations under constructor i.e Initializing state variable, calling superagent and changing the state variable and using the state variable.

Is there any way to mock #defer with react-apollo?

For example, if I want to get a list of planets and the orbital length of them I can do this:
query Planets {
planets {
id
name
orbitalLength
}
}
But let's say it takes a long time to calculate the orbital length and it sometimes throws an error, so I don't want to wait for those values with the first render.
#defer would be a great solution for this problem but it isn't implemented yet, but I could load them one by one and display as they arrive, but I'm not sure, how should it be done.
Maybe something similar could work (but compose won't accept a function as a parameter):
const Feeder = graphql(gql`{
planets {
id
name
}
}`)
const Feeder2 = Feeder(compose(props => (props.data.planets || []).map(planet => {
return graphql(gql`{
orbitalLength(idPlanet: $idPlanet)
}`, {
name: `orbitalLength-${planet.id}`,
options: {
variables: {
idPlanet: planet.id
}
}
})
})))
const FeededComponent = Feeder2(Component)

Resources