useeffect infinite loop even though state data is not changing - javascript

My program goes into an infinite loop constantly calling useEffect() everytime I start the app. I have one state that I don't think is changing other than in the retrieveItemStatus() function so I'm confused on why its going into a loop like it is.
const App = () => {
var items;
const [itemStatuses, updateStatuses] = useState({});
const retrieveItemStatus = async () => {
var tempStatuses;
try {
const value = await AsyncStorage.getItem("#item_Statuses");
if (value !== null) {
tempStatuses = await JSON.parse(value);
//console.log("123456");
} else {
tempStatuses = await JSON.parse(
JSON.stringify(require("../default-statuses.json"))
);
}
updateStatuses(tempStatuses);
} catch (error) {}
};
retrieveItemStatus();
useEffect(() => {
const copyData = async () => {
const itemsCopy = [];
const coll = await collection(db, "Items");
const querySnapshots = await getDocs(coll);
const docsArr = querySnapshots.docs;
docsArr.map((doc) => {
var data = doc.data();
if (itemStatuses[data.name] === "locked") return;
itemsCopy.push(data);
});
items = itemsCopy;
//getItems([...itemsCopy]);
};
copyData();
}, [itemStatuses]);
return (
<View style={styles.container}>
<Text>temp.......</Text>
</View>
);
};

It has nothing to do with useEffect. You're calling retrieveItemStatus unconditionally every time your component function is called to render the componennt. retrieveItemStatus calls updateStatuses which changes state. You see your useEffect callback get run repeatedly as a side-effect of that, because your useEffect callback has itemStatuses as a dependency.
I assume you only need the itemStatuses to get fetched once. If so, put the call in a useEffect callback with an empty dependency array:
useEffect(retrieveItemStatus, []);
Also, you have (note the ***):
const App = () => {
var items // ***
// ...
useEffect(() => {
const copyData = async () => {
// ...
items = itemsCopy; // ***
// ...
};
copyData();
}, [itemStatuses]);
};
That won't work, by the time you assign to items from the callback, anything you might have been trying to do with items will already have just used undefined (the value it gets when you don't give it one). If you need items to be retained, either put it in state (if you use it for rendering) or in a ref (if you don't).
In a comment you said:
Ok so I put retrieveItemStatus() call inside useEffect and removed the dependency which fixed the looping. But now there is an issue where itemStatuses state doesn't get updated before copyData() is called and itemStatuses is needed.. so it doesn't do anything until I manually refresh/render the whole thing again.
If copyData relies on the result from retrieveItemStatus, then put the calls to each of them in the same useEffect, not calling copyData until you get the results from retrieveItemStatus. Something along the lines of the below, though you'll need to tweak it of course as I don't have all the details (I've also made some other comments and changes in there I've flagged up):
// *** There's no need to recreate this function on every render, just
// have it return the information
const retrieveItemStatus = async () => {
try {
let tempStatuses; // *** Declare variables in the innermost scope you can
const value = await AsyncStorage.getItem("#item_Statuses");
if (value !== null) {
tempStatuses = await JSON.parse(value);
//console.log("123456");
} else {
// *** stringify + parse isn't a good way to copy an object,
// see your options at:
// https://stackoverflow.com/questions/122102/
tempStatuses = await JSON.parse(JSON.stringify(require("../default-statuses.json")));
}
return tempStatuses;
} catch (error) {
// *** Not even a `console.error` to tell you something went wrong?
}
};
// *** Similarly, just pass `itemStatuses` into this function
const copyData = async (itemStatuses) => {
const coll = await collection(db, "Items");
const querySnapshots = await getDocs(coll);
const docsArr = querySnapshots.docs;
// *** Your previous code was using `map` just as a loop,
// throwing away the array it creates. That's an anti-
// pattern, see my post here:
// https://thenewtoys.dev/blog/2021/04/17/misusing-map/
// Instead, let's just use a loop:
// (Alternatively, you could use `filter` to filter out
// the locked items, and then `map` to build `itemsCopy`,
// but that loops through twice rather than just once.)
const itemsCopy = []; // *** I moved this closer to where
// it's actually filled in
for (const doc of docsArr) {
const data = doc.data();
if (itemStatuses[data.name] !== "locked") {
itemsCopy.push(data);
}
}
//getItems([...itemsCopy]); // *** ?
return itemsCopy;
};
const App = () => {
// *** A new `items` is created on each render, you can't just
// assign to it. You have to make it a member of state (or use
// a ref if it's not used for rendering.)
const [items, setItems] = useState(null);
const [itemStatuses, setItemStatuses] = useState({});
// *** ^−−−−− the standard convention is `setXyz`.
// You don't have to follow convention, but it makes it easier
// for other people to read and maintain your code if you do.
useEffect(() => {
(async () => {
const newStatuses = await retrieveItemStatus();
const newItems = await copyData(newStatuses);
// *** Do you need `itemStatuses` to be in state at all? If it's
// only used for calling `copyData`, there's no need.
setItemStatuses(newStatuses);
setItems(newItems);
})().catch((error) => {
console.error(error);
});
}, []);
// *** You didn't show what you're using here, so it's hard to be
// sure what needs to be in state and what doesn't.
// Only put `items` or `itemStatuses` in state if you use them for
// rendering.
return (
<View style={styles.container}>
<Text>temp.......</Text>
</View>
);
};
Here are those links as links:
What is the most efficient way to deep clone an object in JavaScript?
Misusing map (on my blog)

#T.J. Crowder's answer is correct but I want to clarify a small point, so you understand why your useEffect didn't stop running.
As you you know, when the dependency in dependency array changes, useEffect runs. But the data in your itemStatuses doesn't change right(according to you)? So why does useEffect re-runs? Let's look at the below example:
const obj1 = {};
const obj2 = {};
const obj3 = {a:2};
const obj4 = {a:2};
console.log(obj1 === obj2)
console.log(obj3 === obj4)
console.log(obj3.a === obj4.a)
As you can see javascript doesn't think that an empty object is strictly equal to another empty object. It is because these objects refer to the different locations in memory regardless of their content.
So that's why every time retrieveItemStatus ran, it updated itemStatuses. Then because itemStatuses got updated(although the value is same), useEffect triggered a re-render, so again everything started all over.

Main reason is in calling retrieveItemStatus(); without any event.
If you want to call this function when page loading, you should like this.
...
useEffect(() => {
retrieveItemStatus()
}, [])
...
This will fix loop issue.

Related

Async fetch and trouble a copy array in class

I have a problem... thant's a code:
class Currency {
cosnstructor() {
this.currencyInfo = [];
}
getCurrency(getInfo) {
this.currencyInfo = getInfo;
}
}
const actuallyCurrency = new Currency;
(async () => {
const response = await fetch(`http://api.nbp.pl/api/exchangerates/tables/A`);
const data = await response.json();
const currency = data[0].rates;
currency.map(element => curArr.push(element));
})();
const curArr = [];
actuallyCurrency.getCurrency(curArr);
this code working good, but I need in this.currencyInfo a new array, not reference to array curArr.
I this this is what you want:
class Currency {
constructor() {
this.currencyInfo = [];
}
getCurrency(getInfo) {
this.currencyInfo = [...getInfo]; // <-- change this line
}
}
const actuallyCurrency = new Currency;
(async () => {
const response = { json: () => { return [{rates:{a:1, b:2, c:3}}];}};
// const response = await fetch(`http://api.nbp.pl/api/exchangerates/tables/A`);
const data = await response.json();
const currency = data[0].rates;
for(key in currency) curArr.push(currency[key]);
actuallyCurrency.getCurrency(curArr);
console.log(actuallyCurrency.currencyInfo);
})();
const curArr = [];
Some thing for you to understand:
1-... is an operator that does a shallow copy of it's argument. So using as above you'll get a new array in currencyInfo.
2-Why actuallyCurrency.getCurrency(curArr); console.log(actuallyCurrency.currencyInfo); have to be inside the function ?
because os the async nature of the operation. Asyncs are postponed to when the execution has finished so the execution arrives in actuallyCurrency.getCurrency(curArr) BEFORE curArr is populated. This makes the internal currencyInfo array being null and not being populated again after execution.
3-Why this currency.map(element => curArr.push(element)); doesn't work ?
Because currency is an object, not an iterable array. If you want to iterate the elements of an object you have to options: get it's keys as an array, iterate this array and then get the value using it's key OR using for...in as I did.
Hope this is enough. Fell free to ask any question you'd like
There are a few improvements to be made. Probably the most important is arranging to check the currencyInfo instance variable after the fetch completes. This and other suggestions indicated by comments...
class Currency {
cosnstructor() {
this.currencyInfo = [];
}
// methods that assign (and don't return anything) ought to be called "set" something
setCurrency(array) {
this.currencyInfo = array;
}
// it probably makes sense to have this class do it's own async initialization
async fetchCurrency() {
const url = `http://api.nbp.pl/api/exchangerates/tables/A`;
// try/catch, so we can respond to failures
try {
const response = await fetch(url);
const data = await response.json();
// no need to map and not sure why the array needs to be copied. I suspect
// it doesn't but [...array] copies array
this.setCurrency([...data[0].rates]);
} catch (error) {
console.log('error fetching', error);
}
}
}
// instantiation requires ()
const actuallyCurrency = new Currency();
// no async/await at the top level
actuallyCurrency.fetchCurrency().then(() => {
console.log(actuallyCurrency.currencyInfo);
})

JS/React/fetch: not getting all data

I'm building an app around the card game Magic, where you can paste a list of cards into a textbox and click a button to show card images. I use fetch and an external site API.
Logging shows no errors, but the result is very inconsistent - sometimes only 1 card is shown, sometimes 2, 4, 7 etc... I need to always render all the data. I've tried for days with this.
const handleClick = async () => {
textarea.split("\n").forEach(async (items) => {
try {
const response = await fetch(
`https://api.scryfall.com/cards/named?exact=${encodeURIComponent(
items
)}&pretty=true`
);
console.log("response.status: ", response.status);
if (!response.ok) {
throw new Error(`Error! ${response.status}`);
}
const result = await response.json();
console.log("result" + result);
objects.sort(() => Math.random() - 0.5);
objects.push(result);
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
setScrydata(objects);
}
});
};
From the console.log I see everything is fetched, but sometimes in "batches", for example first 2, then 5. This is the problem I think(?) cause only 2 cards are then rendered. Is Promise.all the solution somehow? I've tried it but couldn't get it to work, I changed
const result = await response.json();
to
const result = await Promise.all([response.json()]);
but it doesn't work, the results are still in "batches".
Big thank you in advance for any help.
Edit, this is how I render the images:
{scrydata.length > 0 && (
<img
src={scrydata[0].image_uris?.normal}
key={scrydata[0].id}
alt="asdf"
/>
)}
{scrydata.length > 1 && (
<img
src={scrydata[0].image_uris?.normal}
key={scrydata[0].id}
alt="asdf"
/>
)} ... etc
Up to seven, which is the max. This shows images, but it's inconsistent; I want seven every time. Maybe this code could be my problem.
Thanks again.
You want to split this up into two separate stages:
get all the images (with undefined or false for any failures)
update the UI once you have those.
But what you've written doesn't do that, instead it does:
for each card:
get its image
update the UI irrespective of whether that get succeeded
So rewrite your logic a little:
async function getCardImage(title = ``) {
// do we need to do anything?
if (title === ``) return false;
try {
const response = await fetch(...);
if (!response.ok) throw new Error(`Error! ${response.status}`);
return await response.json();
} catch (e) {
console.log(e);
}
// our function always returns "normal" data, it never exits by throwing.
// That way, we can easily filter failures our of the Promise.all result
return false;
}
async function fetchCardImages(titles = [], setIsLoading=()=>{}) {
// do we need to do anything?
if (titles.length === 0) return titles;
setIsLoading(true);
const results = (
await Promise.all(titles.map(getCardImage))
).filter(Boolean);
setIsLoading(false);
return results;
};
With handleClick changed to:
const handleClick = async (_evt) => {
const titles = textarea.split(`\n`).map(s => s.trim()).filter(Boolean);
// do we need to do anything?
if (titles.length === 0) return;
const updates = await fetchCardImages(titles, setIsLoading);
updateScryData(updates);
// Don't update `objects` until you're in the
// updateScryData function: your event handler
// itself should have zero code that directly
// manipulates any data.
//
// And of course, make sure `updateScryData` checks
// the passed argument, so that if it's an empty
// array, it just immediately returns because no
// work needs to be done.
}
Although I'd recommend not calling it "handle click" because the fact that you're pointing to it from an onClick React attribute already tells people that. Give functions names based on what they do: in this case, getNewCardImages or something.
And then, finally, you render your scryData using a map:
function generateCardElement({ id, image_uris, description }) {
const src = image_uris?.normal;
if (!src) return;
return <li key={id}><img src={src} alt={description}/></li>;
}
...
render() {
return
...
<ul className="card-images">{scryData.map(generateCardElement)}</ul>
...
}

setState in useEffect not update

Got an input and want to pass value to handler:
const [term, setTerm] = useState('');
<Input type="text" onBlur={(e)=>handleFilter(e, 'params')} />
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}
I want to make an search filter function, for ex. if I enter a, console return empty, then I enter b console return a ! it means term not update immediately, then I used useEffect but inside the useEffect I got new value, but inside handleFilter function still console return prev value.
useEffect(() => {
getApi()
.then(data => {
console.log(data)
})
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])
I tried this but no success:
setTerm({...term, e.target.value});
Any solution? I'm new to react hook.
You can check this answer here.
This is because react's state update is async. You can't rely on its update right after calling setState. Put your effects (code that is run after a state is updated) in a useEffect hook.
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
}
React.useEffect(() => {
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}, [term]);
setTerm is async and will update the term on the next render cycle.
it is not updated immediately for the current render cycle.
you can store the current value in a ref if you are curious what is happening behind the scenes
const termRef = React.useRef(term);
termRef.current = term;
const yourHandler = () => {
setTimeout(() => console.log(termRef.current), 0);
}
If we are going ahead with useEffect with an API call, please ensure to include async await scenario to ensure that setState updates after the data is fetched.
useEffect(async () => {
const data = await getApi()
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])
I could be wrong on this one, but could this have something to do with you calling an async function from a normal event. This might cause some type of delay.
There is also the fast that useEffect is treated differently in React than a normal function, since it's integrated into React.
It could also be related to the [term] trigger in the useEffect but the event in your handleFilter isn't treated the same

React Hooks, setState not working with an async .map call

So, I'm trying to change a state in my component by getting a list of users to call an api.get to get data from these users and add on an new array with the following code:
function MembersList(props) {
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => {
return dev.login;
});
handleDevs(arr);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [membersList]);
function handleDevs(membersArr) {
membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
setDevs([...devs, data]);
});
}
but the devs state always return an empty array, what can I do to get it to have the actual users data on it?
The issue you were having is because you were setting devs based on the data from the original render every time due to the closure around handleDevs function. I believe this should help take care of the issues you were having by using the callback method of using setDevs.
This also takes care of some issues with the dependency arrays and staleness in the useEffect hook. Typically using // eslint-disable-next-line react-hooks/exhaustive-deps should be your last resort.
function MembersList(props) {
// this isn't needed unless you are using it separately
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => dev.login);
arr.forEach(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
setDevs((devs) => [...devs, data]);
})
}, [membersList]);
}
You need to understand how React works behind scenes.
In short, it saves all the "sets" until it finishes the cycle and just after that actually update each state.
I think that why you do not see the current state updated.
For better understanding read this post: Medium article
The issue is that setDevs uses devs which is the version of devs when handleDevs is defined. Therefore setDevs will really only incorporate the data from the last time setDevs is called.
To fix this you can use the callback version of setDevs, like so:
setDevs(prevDevs => [...prevDevs, data])
Also since you are not trying to create a new array, using map is not semantically the best loop choice. Consider using a regular for loop or a forEach loop instead.
you call setDevs in the async execution loop. Here is the updated handleDevs function.
function handleDevs(membersArr) {
const arr = membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
return data;
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
});
setDevs([...devs, ...arr]);
}

Hanging in a vue component with async / await call

I make my first steps with the vue framework and I can't figure out to solve the following problem.
dateClick(arg)
{
this.cal.date = arg.date;
this.cal.title = arg.resource.title;
this.cal.resource = arg.resource.id;
// const slots = (async() => {
// return await dataService.getSlots(arg);
// })().catch(console.error);
// console.log(slots);
(async() => {
const slots = await dataService.getSlots(arg);
console.log(slots);
})().catch(console.error);
}
The console.log in the async function works correct and returns the slots. Finally I would need to also set a data attribute in the current Vue Component like this.cal.slots = slots. But that doesn't work, always undefined. I also tried the commented code above to return the await - this would result in "Promise {pending}". Can't figure it out how to solve this?
Try like this:
async dateClick(arg) {
...
this.cal.slots = await dataService.getSlots(arg);
...

Categories