React useEffect loop - reactjs

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.

Related

Unable to understand setState of react js function paramters in a specific call?

setListOfPosts(curPosts => {
let newPosts = [...curPosts];
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
}
});
//is curPosts an instance of array or complete array?? my listofPosts is an array of objects
Your setState call needs to return newPosts, and you're creating an array using the spread operator which is why it's coming back as an array of objects.
I'm not sure what your desired output is, but by adding a return function it will set the state:
setListOfPosts(curPosts => {
let newPosts = [...curPosts];
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
return newPosts
}
});
This is untested but if your logic is correct should return an array of objects with the objects alert value updated.
Another option would be to do your logic before your setState call, by creating a a newState array and then simply updating the state with that new array without the use of the callback.
The callback function is useful if you want to add a new object to state array or do something that preserves the initial state, in your example you could do it without the callback like this:
// Create a copy of the state array that you can manipulate
const newPosts = [...newPosts]
if (data.response) {
// Add your logic to the state copy
newPosts[newPosts.findIndex(p => p.id === postId)].alert = response.data;
// Replace state with state copy
setListOfPosts(newPosts)
}
Again untested but hopefully this should help you understand the use of the callback function and the right way to use it.

How to correctly modify the state from the parent component before passing it down to child component

I have three components:
1st component
--2nd component
----3rd component
I need to pass a state and its handleState down from the first component to the second one. In the second one I need to filter through the state array values, I also need to update the db and the state so I make multiple API and handleState calls in a for loop inside of useEffect(). The third one renders the state data.
Here's the second component:
export default function LearnNewApp({ deck, handleShowAppChange, handleDecksChange }) {
let words = deck.words.filter(x => x.wordGroup == "newLearning")
useEffect(() => {
let unsubscribed = false;
if (words.length < 10) {
let vacant = 10 - words.length
let newDeck = JSON.parse(JSON.stringify(deck))
let unseenWords = newDeck.words.filter(x => x.wordGroup === "newUnseen")
for (let i = 0; i < vacant && i < unseenWords.length; i++) {
let wordUnseenToLearning = unseenWords[i]
wordUnseenToLearning.wordGroup = "newLearning"
callAPI(wordUnseenToLearning.id, "newLearning")
if (!unsubscribed) handleDecksChange(wordUnseenToLearning)
}
}
return () => unsubscribed = true;
}, [])
function memorized(word) {
callAPI(word.id, "first")
let wordLearningToFirst = {
...word, wordGroup: "first"
}
handleDecksChange(wordLearningToFirst)
}
function showAgain() {
}
if (words != []) return (
<CardPage words={words} handleShowAppChange={handleShowAppChange} leftButtonFunc={memorized} rightButtonFunc={showAgain} />
)
}
The state array that's being modified is an array of decks with words. So I'm trying to narrow down the chosen deck with .filter to up to ten words so they could be shown to the user in the third component. To do that I check how many words there are with showNew attribute. If there's less than ten of them I check if there are any new words in the deck to change from neverShown to showNew.
The code causes an error probably because it takes some time to do everything in useEffect(), not to mention it runs after render.
The third component has a button that triggers a function from the second component that also updates the db and state but not in a loop.
So the main problem is that I don't know how to properly fix the deck modification and subsequent render in the second component.

Firestore navigation for onUpdate and shuffle an array

I have this Firebase structure:
Firebase Structure.
Then I have a function in my Code, which adds a map called "set".
My Structure is looking like this after: New structure.
Now i want an onUpdate Firebase function, which is called after the map "set" is added in any document.
This function should shuffle my "question" array.
I tried something like this:
exports.shuffleSet = functions.firestore
.document('duell/{duell_id}/set/questions')
.onUpdate((change, context) => {
const data = change.after.data();
const previousData = change.before.data();
if (data.name == previousData.name) {
return null;
}
//shuffle code here
});
But Im not sure if .document('duell/{duell_id}/set/questions') is the correct way to navigate to the question array. And at the beginning the "set" is not even existing as explained at the top.
How do I navigate to the question array correctly, that I can pull it & update it shuffled?
You should pass a document path to functions.firestore.document(). You cannot pass a field name, since Firestore Cloud Functions are triggered by documents events.
So you should do as follows:
exports.shuffleSet = functions.firestore
.document('duell/{duell_id}')
.onUpdate((change, context) => {
if (!change.after.data().shuffledSet) {
const data = change.after.data();
const question = data.set.question; // get the value of the question field
const shuffledSet = shuffle(question); // generate the new, suffled set. It’s up to you to write this function
return change.after.ref.update({shuffledSet});
} else {
return null; // Nothing to do, the shuffled field is already calculated
}
});

unable to set record in react usestate array from sqlite

I am using sqlite for local storage in react native but when I tried to fetch record and set it into the useState , it doesn't update the state
here is the code
const SelectQuery=async()=>{
const token = await AsyncStorage.getItem('docToken')
let selectQuery = await ExecuteQuery("SELECT * FROM DoctorConversations where d_phone=?",[token]);
var rows = selectQuery.rows;
var temp = [];
for (let i = 0; i < rows.length; i++) {
temp.push(rows.item(i))
}
return temp;
}
This is how I am calling
SelectQuery().then(res=>res.map(item=>{
setFlatListItem([...flatListItem,item])
}))
I guess that you have:
const [flatListItem, setFlatListItem] = useState([]);
setFlatListItem changes internal state and schedules a rerender. flatListItem gets a new value only on the next render.
You need to use functional updates to modify the current internal state:
setFlatListItem(state => [...state, item])
If you want to replace the whole list, you do not need to map individual items:
SelectQuery().then(res=> setFlatListItem(res))
Note that flatListItem is a bad name in your case since it holds an array. You should probably rename it to just [flatList, setFlatList].

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
) ...

Resources