Too many re-renders/infinite loop - setting state in mapping function - javascript

I am trying to map through items in my cart and compare the id of the item to the id returned in the data object, and then displaying some info on the browser such as quantity for each item etc. I am also trying to add a total cart items display element, however, when I add the quantity of each item in the cart (eg one item has quantity of 3, another has 1 etc) and try to set the state within the mapping function, I get a re-render error. I understand from reading online that its because when you set state, the component re-renders, however, I am unsure how to set state with the total quantity of items in the cart without setting state from within the function.
I also tried declaring the variable outside the mapping function and adding to that variable on each iteration, but that does not work either.
IF i console log cartItemsTotal within the mapping function, I get the correct value of the addition of all the items being mapped through's quantity.
If I console log it outside the mapping function, its always 0. Is this because of scope? You cant manipulate a variable outside of the scope of the function?
Any help is appreciated.
Thanks
const CartIconSummary = ({data}) => {
const [cartQuantity, setCartQuantity] = useState()
const deleteCart = useCartDelete()
const cart = useCart()
let cartTotal = 0
let vat = cartTotal * .23.toFixed(2)
let cartItemsTotal = 0
const displayCartItems = () => {
setCartQuantity(cartItemsTotal)
return cart && cart.map((cartItem, index) => {
return data && data.map((dataItem) => {
if(cartItem.id === dataItem.id) {
cartTotal = cartTotal + dataItem.price * cartItem.quantity
vat = vat + (dataItem.price * cartItem.quantity) * .23
cartItemsTotal = cartItemsTotal + cartItem.quantity
setCartQuantity(cartItemsTotal)
return (
<div key={index} className={`${className}CartItemContainer`}>
<div className={`${className}ImageContainer`}>
<img className={`${className}Image`} src={data ? `${process.env.PUBLIC_URL}${dataItem.cartImage.slice(1)}` : ""} alt="cart-item-preview"/>
</div>
<div className={`${className}CartItemNamePriceContainer`}>
<div className={`${className}CartItemName`}>
{dataItem.shortName}
</div>
<div className={`${className}CartItemPrice`}>
${dataItem.price}
</div>
</div>
<div className={`${className}CartItemQuantity`}>
x {cartItem.quantity}
</div>
</div>
)
} else {
return ""
}
})
})
}
return (
<>
<div className={`${className}CartDeleteContainer`}>
<div className={`${className}CartQuantity`}> CART({cartQuantity ? cartQuantity : 0}) </div>
<div className={`${className}CartDelete`} onClick={() => deleteCart([])}> Remove all </div>
</div>
{displayCartItems()}
</>
)
}

Currently you call setCartQuantity inside the function displayCartItems, which means that whenever the component renders the function displayCartItems will run again as well. That means when the function displayCartItems executes, it will change the state cartQuantity with setCartQuantity and if a state changes the whole component will re-render, which means displayCartItems will be exectued again.
To solve this, you should use a useEffect hook and add cartItemsTotal in the dependency array so that setCartQuantity is only executed when cartItemsTotal changes.

Related

Remove element from useState array by index

SOLUTION: Update the key value for the input element to refresh the default value => content of the input element. Deleting an element from the array DID work. Thanks for your help!
src: https://thewebdev.info/2022/05/12/how-to-fix-react-input-defaultvalue-doesnt-update-with-state-with-javascript/#:~:text=state%20with%20JavaScript%3F-,To%20fix%20React%20input%20defaultValue%20doesn't%20update%20with%20state,default%20value%20of%20the%20input.
I got an useState array in my code which represents a lisst of students:
const [students, setStudents] = useState([""]);
This array gets mapped to student elements:
{students.map((student, index) => <Student setStudents={setStudents} students={students} id={index} key={index} content={student} />)} I also got an AddStudent element which adds students to the array.
function AddStudent(props) {
const {setStudents} = props;
return (
<button className="change-student add-student" onClick={() => {
setStudents((students) => [...students, ""])
}}>
+
</button>
);
}
The RemoveStudent component is supposed to remove a student by its index in the array. I've tried many different ways but none worked correctly. How can I get it to work? Here is my code:
function RemoveStudent(props) {
const {students, setStudents, id} = props;
return (
<button className="change-student remove-student" onClick={() => {
let data = students;
if(id > -1) {
data.splice(id, 1);
}
console.log(data)
// setStudents(data)
// alternative:
// setStudents(students.filter(index => index !== id)); // removes the last element in the list
// doesn't work properly
}}>
-
</button>
)
}
Thanks for your help!
2 things should be noted here:
While updating react state arrays, use methods that return a new array (map, filter, slice, concat),
rather than ones that modify the existing array (splice, push, pop, sort).
While updating React state using its previous value, the callback argument should be used for the state setter. Otherwise you may get stale values. (See React docs).
if(id > -1) {
setStudents(students=> students.filter((s,i)=>(i != id)))
}
Consult this article, for a complete reference about how to update React state arrays.
You need to copy the students array first and then try removing the student by index. I assume by id you mean index at which to remove the student. Then you can try something like:
function RemoveStudent(props) {
const {students, setStudents, id} = props;
return (
<button
className="change-student remove-student"
onClick={() => {
if(id > -1) {
const data = [...students]; // making a copy
data.splice(id, 1); // removing at index id
console.log(data)
setStudents(data)
}
}}
>
-
</button>
)
}
With array.filter() you have a mistake in how you pass callback to filter() method. Please try the following:
setStudents(students.filter((,index) => index !== id));
Notice the index is second param of the callback so I used a , before index.
After #Irfanullah Jan 's answer you should make sure how you show the student.
Here is the simple example:
const [students, setStudents] = useState([1, 2, 3]);
return (
<div>
{students.map((student, index) => {
return <div>{student}</div>; // show the value not the index
})}
<button
onClick={() => {
let id = 1;
const copy = [...students];
copy.splice(id, 1)
console.log(copy)
setStudents(copy);
}}
>
-
</button>
</div>
);
The code above will delete the student of "index==1"

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. - React

So I'm trying to make a screen where data from user's localstorage is used (Lets call it var1) but I'm getting Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. error. What I'm trying to do is check if the data from user's localstorage exists and if it does then it will put that data into a state but first it will grab another variable from localstorage (User auth token, lets call it var2) and put it into every object in var1 (var1 is a list which contains objects) and this is done using map then the state is set to the changed var1 with the auth token(or var2), then it returns some HTML and some logic is used in HTML, For every object in var1 it will create a new select tag with numbers ranging from 1 to 20 and this is done using mapping an array with 20 numbers (I'm doing this because I could not get for loop to work properly) and if the current number of option in select tag matches a key value pair in one of var1's object then it will
select the option tag or put selected attribute on option tag and if you change the value of select tag then it will trigger a function which will map through var1 and select the object which user requested and change the value of quantity to whatever the user selected on select tag. I tried to cut down and simplify my code as much as I could. My code is like this:
function RandomScreen() {
const [var1, setvar1] = useState([])
let localstoragevar = JSON.parse(localStorage.getItem('var'))
let newCart = []
if (localstoragevar) {
localstoragevar.map(item => {
item.authtoken = localStorage.getItem('AuthToken')
newCart.push(item)
})
}
setvar1(newCart)
let twenty = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
return (
{var1.map(newItem => {
{/* HTML code goes here */}
{twenty.map(number => {
if (number == item.quantity) {
return (
<option onChange={handleClick} selected name={newItem.id} value={newItem.quantity}>{newItem.quantity}</option>
)
} else {
return (
<option onChange={handleClick} name={newItem.id} value={number}>{number}</option>
)
}
})}
})}
)
}
Your render calls setvar1 which in turn trigger's a re-render.
You should put this whole logic inside a useEffect hook:
useEffect(() => {
let localstoragevar = JSON.parse(localStorage.getItem('var'))
let newCart = []
if (localstoragevar) {
localstoragevar.map(item => {
item.authtoken = localStorage.getItem('AuthToken')
newCart.push(item)
})
}
setvar1(newCart)
}, []);
This is what you have to do is to avoid logic in your render function. For that case, we do have useEffect function plus on top of that you may add useMemo;

how to stop rerendering in react on checkbox check?

I have simple list which is dynamically added on add button click. in my list there is a checkbox is also present .so I have an issue when I toggle the checkbox my whole list is re render why ?
let take example I added A,B,C,D in my list when I toggle D checkbox it should only render D item currently it render whole list why ?
here is my code
https://codesandbox.io/s/stupefied-wildflower-gv9be
const Item = ({ text, checked, onCheckedHandler }) => {
console.log(checked, "ssss");
return (
<div className={checked ? "bg" : ""}>
<span>{text}</span>
<input type="checkbox" onChange={e => onCheckedHandler(e, text)} />
</div>
);
};
Every time items changes (whether by adding a new item or checking a value), you are creating a new onCheckedHandler in your App. This propagates down to your Item component. Since the previous onCheckedHandler property is not referentially equivalent to the previous one, it renders (and you see that console log for each item). Memoizing the component alone won't help because a property being passed to it is changing every time.
To get around that, you need to memoize the onCheckedHandler, try this:
const onCheckedHandler = useCallback((e, selectedText) => {
const target = e.target
setItems(items => {
const i = items.findIndex(i => i.text === selectedText);
let obj = items[i];
obj.checked = target.checked;
return [...items.slice(0, i), obj, ...items.slice(i + 1)];
})
}, [setItems])
The you can wrap your Item compoennt with React.memo, and it should work as expected. You'll also need to import the useCallback the same way you import useState

Change style dynamically, without resetting when re-rendered in React

I have a page of just a list of divs calling an onClick function. Pretty much this...
<div
className="square card-container bg-primary"
points="200"
id="2"
onClick={e => select(200, "cat1", e.target.id)}
>
<h3 className="text-light">200</h3>
</div>
Just 30 of them. With sequential ID's. It's easy enough to push the ID's to a new array when clicked, or to add className once they're clicked, and when I step through the code in Debugger I can see that the class is Added and the styles change, but it is immediately set back to the previous "Un-clicked" className. I was thinking that it might work to make each of these div it's own component and than use useState, but since they're all mostly identical I would still need a key or id, so I'm not sure how I would pass that in to a state and use that conditionally? I'm sorry if this doesn't make sense. Basically I just want to change styles slightly once an item has been selected, without going back to the initial className when it is re-rendered.
This is the script that gets called onClick.
const selected = []
const change = id => {
selected.push(id);
console.log(selected);
};
const select = (points, cat, id) => {
let newArr = questions.filter(
q => q.category === "Sample 1" && q.points === points
);
change(id);
if (newArr.length > 1) {
let randomOutput = Math.floor(Math.random() * newArr.length);
console.log(newArr[randomOutput]);
let out = newArr[randomOutput];
props.setQuestion({ out });
props.detail();
return out;
} else {
let out = newArr;
props.setQuestion({ out });
props.detail();
console.log(points, cat);
}
}
You need to store a list of selected items using state and make className dynamic
Example: https://jsfiddle.net/78bsnhgw/

Why counter defined with useRef hook is not incrementing on each render?

I am trying use the hook useRef() to keep a counter of which items to display next, and increment the counter on each render and using the counter value to slice in elements in an array. On each render the counter index.current should increase by 10 but in my case its not increasing and I keep getting the same 10 elements over and over. I can't figure out why. The collection overview component here is mapping over the collection preview component 4 times.
//CollectionPreview
const CollectionPreview = ({title,movieItems,Counter}) => {
const index = React.useRef(0)
const movieData = movieItems.slice(index.current, index.current + 10)
index.current += 10
return (
<div className="collection-preview">
<h1 className="title">{title.toUpperCase()}</h1>
<div className="preview">
{console.log(movieData)}
</div>
</div>
);
}
const mapStateToProps = state => ({
movieItems: selectMovieItems(state),
})
export default connect(mapStateToProps)(CollectionPreview);
//CollectionOverview
class CollectionOverview extends React.Component {
render() {
return (
<div className="collection-overview">
{
CollectionData.map(items =>
<CollectionPreview key={items.id} title={items.title} />)
}
</div>
);
}
}
export default CollectionOverview;
Personally I think counting the number of renders is bad pattern, as renders can be called multiple times in different occasions and its difficult to keep track of every single render call as your app gets bigger.
I think a better approach is to have a initialIndex or pageIndex in some state somewhere. As you are using Redux, I think you should put that in the reducer and create the appropriate actions to increment this value.

Categories

Resources