React updating state/setState in .map causes infinite loop - javascript

I realize that setTotal(newState) is causing an infinite loop because when it gets updated, it re-renders > causes the function 'showDiscounted' to be called again > state gets updated > and it just goes on forever.
I know that I have to use useEffect to stop this issue, but I'm just not quite sure how I can implement it. I've thought about using useRef because it doesn't cause rerender, but I need it to rerender to show the state 'total' somewhere else. What is the best way to go about this?
import { useEffect, useState } from "react";
const QuoteCalcBot = ({ parts, setParts, option, discount }) => {
const [total, setTotal] = useState([0,0,0]);
const [discountedTotal, setdiscountedTotal] = useState([0,0,0]);
//change individual discount for a part
const handleDiscountChange = (part, e) => {
console.log(e.target.value);
console.log(part);
}
//takes in the full price then shows the discounted price of a part. Also adds up the discounted price to discountedTotal for displaying it later
const showDiscounted = (price) => {
const temp = Math.ceil((price * ((100 - discount) / 100)) / 36)
const newState = discountedTotal.map((t, i) => i === option? t + temp : t)
console.log(newState);
setTotal(newState); //this causes infinte loop ERROR
return (
<div className="col-2">
{temp}
</div>
)
}
const addToTotal = (price) => {
//we need to add the full prices to total so it can display
return (
Math.ceil(price / 36)
)
}
//this also works.
//const showParts = (activeIndex) => { return parts.map(part => part.active[activeIndex] && <div key={part.bodyPart}>{part.bodyPart}</div>) }
const showParts = (activeIndex) => {
return parts.map(
(part) => part.active[activeIndex] && (
<div key={part.bodyPart} className="row">
{/* body part */}
<div className="col-3">{part.bodyPart}</div>
{/* original full price */}
<div className="col-2 text-decoration-line-through">${addToTotal(part.price)}</div>
{/* discounted price */}
<div className="col-2">
${(showDiscounted(part.price))}
</div>
{/* choose discount */}
<div className="col-auto">
<select className="form-select-sm" aria-label="Default select example" /* value={discount} */ onChange={(e) => handleDiscountChange(part, e)}>
<option defaultValue>{discount}</option>
<option value="30">30%</option>
<option value="40">40%</option>
<option value="50">50%</option>
<option value="60">60%</option>
</select>
</div>
<div className="col-1 text-end"><button type="button" className="btn-close" aria-label="Close"></button></div>
</div>)
)
}
return (
<div className="">
<div className="row rows-cols-3">
{/* to keep this part cleaner, having a separate state that filters out is ideal */}
<div className="col-4">
{showParts(0)}
</div>
<div className="col-4">
{showParts(1)}
</div>
<div className="col-4">
{showParts(2)}
</div>
{/* TOTAL */}
<div className="col-4">discounted total for 36 months: {total[0]}</div>
<div className="col-4">total, per month, cost, savings</div>
<div className="col-4">total, per month, cost, savings</div>
</div>
</div>
);
}
export default QuoteCalcBot;

Can try to update the state from the event handler(source of the event)
//passing extra params to event handler
const handleDiscountChange = (part, e, partPrice) => {
// this may need extra props to update the price for the right part
const newDiscount = e.target.value
// put this in a common function
const calcPartPrice = Math.ceil((partPrice * ((100 - newDiscount) / 100)) / 36)
const newState = discountedTotal.map((t, i) => i === option? t + calcPartPrice : t)
setTotal(newState);
// update discount
console.log(e.target.value);
console.log(part);
}
better to move this outside as a seperate component
const ShowParts = ({ parts = [], activeIndex, discount, handleDiscountChange, addToTotal }) => {
return parts.map(
(part) => {
const partPrice = Math.ceil((part.price * ((100 - discount) / 100)) / 36)
return part.active[activeIndex] && (
<div key={part.bodyPart} className="row">
{/* body part */}
<div className="col-3">{part.bodyPart}</div>
{/* original full price */}
<div className="col-2 text-decoration-line-through">${addToTotal(part.price)}</div>
{/* discounted price */}
<div className="col-2">
<div className="col-2">
{partPrice}
</div>
</div>
{/* choose discount */}
<div className="col-auto">
<select className="form-select-sm" aria-label="Default select example" /* value={discount} */
// pass partPrice and other props needed to handler to help in total calculations
onChange={(e) => handleDiscountChange(part, e, part.price)}
>
<option defaultValue>{discount}</option>
<option value="30">30%</option>
<option value="40">40%</option>
<option value="50">50%</option>
<option value="60">60%</option>
</select>
</div>
<div className="col-1 text-end"><button type="button" className="btn-close" aria-label="Close"></button></div>
</div>)
})
}
usage inside main app
<div className="col-4">
<ShowParts
parts={parts}
activeIndex={0}
discount={discount}
handleDiscountChange={handleDiscountChange}
addToTotal={addToTotal}
/>
</div>
The code can be optimized further, but I hope this helps you in some way

Related

How do I update a specific index

So I'm practising react with a simple task master app, where I add each user input as a new task in an array of tasks. In the app, I have Add, Delete, and Update buttons.
Everything seems to be working fine except for the update function, it updates the last index of the array instead of the specific index I clicked.
Here is my JSX
const JsxElement = task.map((eachTask, index) => {
return (
<Fragment key={index}>
<div key={index} className="table-data-container">
<div className="item-data">{eachTask}</div>
<div className="item-data">{date}</div>
<div className="item-data">
<div className="btn-data-container">
<div className="btn-data">
<div className="btn" onClick={() => deleteTask(index)}>Delete</div>
</div>
<div className="btn-data">
<div className="btn" onClick={() => UpdateTaskBtn(eachTask, index)}>Update</div>
</div>
</div>
</div>
</div>
<br/>
{task.length - 1 === index &&
<div className="input-update-container">
<div className="input-area">
<input
ref={inputRef}
type="text"
/>
</div>
<div className="btn-update-add-container">
{update ?
<div className="btn-add" onClick={() => handleTaskUpdate(eachTask, index)}>Update
Task</div>
:
<div className="btn-add" onClick={handleTask}>Add Task</div>
}
</div>
</div>
}
</Fragment>
)
})
The first update button function prepares the input, sets the task to be updated and makes the update button visible. The second one is where I want the update action to happen once clicked.
function UpdateTaskBtn(eachTask) {
inputRef.current.value = eachTask
setUpdate(true)
}
function handleTaskUpdate(e, index) {
const list = [...task]
list[index] = inputRef.current.value
setTask(list)
inputRef.current.value = ""
setUpdate(false)
}
I want to be able to set the task to the specific index I want to update.
based on this line of code
{task.length - 1 === index &&
you are checking if the index is the last index , so you are passing the last index to the handleTaskUpdate function. so you can define a updateIndex state
const [updateIndex,setUpdateIndex] = useState()
then your function should look like this
function UpdateTaskBtn(eachTask,index) {
inputRef.current.value = eachTask
setUpdateIndex(index)
setUpdate(true)
}
function handleTaskUpdate(e, index) {
const list = [...task]
list[updateIndex] = inputRef.current.value
setTask(list)
inputRef.current.value = ""
setUpdate(false)
}
do not forget to pass index to UpdateTaskBtn function in onClick event
Try this
function handleTaskUpdate(e, index) {
const value = inputRef.current.value
const updatedTasks = task.map((task, i) => i === index ? value : task)
setTask(updatedTasks)
inputRef.current.value = ""
setUpdate(false)
}

React Tab is not letting me edit text field, getting out of focus

When i type, the textbox is getting out of focus. I am able to type only one letter at a time. Following is my code: -
<TabPanel value={value} index={0}>
{[...Array(commentCount),].map((item, index) => {
return (
<>
<div className="col-12 d-flex comments-content">
<div className="mb-0 flex-10">
<textarea name="" id="" rows="4" className="w-100 p-2 mb-3" data-testid={"commentTextArea_" + index}
value={ commentArea.find(x=>x.id==index)?.value==undefined?"":commentArea.find(x=>x.id==index)?.value}
onChange={(e)=>{setComment(e,index)}}/>
</div>
</div>
</div>
and my js code is :-
const [commentArea,setCommentArea]=useState([{value:"",id:0}]);
const setComment=(e,index)=>{
const searched= commentArea.find(x => x.id === index);
if(searched!="" && searched!=undefined){
searched.value=e.target.value;
}
else{
let res={
value:e.target.value,id:index
}
commentArea.push(res);
}
setCommentArea([...commentArea]);
}
Don't use keys that are constantly changing.
You can read about keys here: Keys
Your textfield should have a event handler function looking like this:
const handleChange = (event) => {
setCommentArea(event.target.value);
};

How to prevent random numbers from generating again?

I am building some kind of calculator for 24 game. Everything was fine until I added, onClick to the button to store the state.
Here's the fullcode:
import React from "react";
import { useState } from "react";
export default function DuaEmpat() {
const [calc, setCalc] = useState("");
const [result, setResult] = useState("");
const ops = ["/", "*", "+", "-"];
const ns = () => Math.floor(Math.random() * (9 - 1 + 1)) + 1;
const updateCalc = (value) => {
setCalc(calc + value);
};
const createDigits = () => {
const digitz = [];
for (let i = 1; i < 5; i++) {
digitz.push(
<button
onClick={() => updateCalc(ns().toString())}
className=""
key={i}
>
{ns()}
</button>
);
console.log(digitz);
}
return digitz;
};
return (
<>
<main>
<div className="App ">
<div className="Calculator-24 ">
<div className="display">
{result ? <span className="text-gray-400">(0)</span> : ""}{" "}
{calc || "0"}
</div>
<div className="operators r">
<button className="flex-1" onClick={() => updateCalc("/")}>
/
</button>
<button className="flex-1" onClick={() => updateCalc("*")}>
*
</button>
<button className="flex-1" onClick={() => updateCalc("+")}>
+
</button>
<button className="flex-1" onClick={() => updateCalc("-")}>
-
</button>
<button className="flex-1">DEL</button>
</div>
<div className="digits flex flex-wrap appearance-none border-none outline-none bg-zen-tertiary font-bold text-white text-2xl p-4 cursor-pointer">
{eval(createDigits())}
<button
onClick={() => window.location.reload()}
className=" p-4 ">
acak ulang
</button>
<button className=" p-4 ">=</button>
</div>
</div>
</div>
</main>
</>
);
}
Any idea on how to stop the value of generated numbers( ns() ) to be unchanged?
I think this is what you are trying to do? Please tell me if i misread your question.
In your code you have a ns() function. Every time that runs, it will create a new random number. So lets break down the code you wrote.
<button
onClick={() => updateCalc(ns().toString())} //When clicked this will run the ns function and create a new number. Then it will pass that value to the updateCalc funtion.
className=""
key={i}
>
{ns()} //this is calling the ns function AGAIN to create a new value. Therefore your onClick value and your button text will always mismatch.
</button>
So how can we fix your code? Lets only call the function once!
for (let i = 1; i < 5; i++) {
let randomNumber = ns().toString(); //Calls the function ONCE for every for loop and saves the return to a variable.
digitz.push(
<button
onClick={() => updateCalc(randomNumber)} // Lets use that variable from earlier
className=""
key={i}
>
{randomNumber} // lets use that same variable again.. Remember the function was only ran once during this loop. so the variable will be EXACTLY the same until the next loop.
</button>
);
console.log(digitz);
}

Make a button expand and collapse in map function in ReactJS

I am working on a project as a means to practice some stuff in react and I need to render a button for each of the map data. I did this successfully but expand and collapse has been giving me issue. Whenever I click on the button all data collapse and expand together.
const DataFetch = () => {
...
const [btnValue, setBtnValue] = useState('+');
const handleChange = (e) => {
setShowData(!showData);
setBtnValue(btnValue === '+' ? '-' : '+');
};
return (
<div className='container'>
...
{studentResults
.filter((val) => {
if (searchTerm === '') {
return val;
} else if (
val.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
val.lastName.toLowerCase().includes(searchTerm.toLowerCase())
) {
return val;
} else {
return null;
}
})
.map((student) => {
return (
<div key={student.id}>
<div className='card'>
<div className='row'>
<div className='col-2'>
<div className='pic'>
<img src={student.pic} alt='avatar' />
</div>
</div>
<div className='col'>
<div className='details'>
<p className='name'>
{student.firstName.toUpperCase()}{' '}
{student.lastName.toUpperCase()}
</p>
<div className='sub-details'>
<p>Email: {student.email}</p>
<p>Company: {student.company}</p>
<p>Skill: {student.skill}</p>
<p>
Average:{' '}
{student.grades.reduce(
(a, b) => parseInt(a) + parseInt(b),
0
) /
student.grades.length +
'%'}
</p>
<button onClick={handleChange} className='showBtn'>
{btnValue}
</button>
{showData && (
<div>
<br />
{student.grades.map((grade, key) => {
return (
<p key={key}>
Test {key + 1}:   {grade}%
</p>
);
})}
</div>
)}
...
Collapse Image
Expand Image
All the elements expand and collapse together because you assign to all of them the same state showData state.
One solution would be to add a new field to your data (so inside student) that is true or false when you want to expand or collapse the single student.
Another solution would be to create the showData state as an array where each element correspond to a different student. When you click the button, in this case, you pass to the function for example the id and with that you link your student to the right element inside the showData.

select tag is keeping initial state value and not updating

I'm having some issues updating a select tag value. When I click on it and try changing the value it logs the new value to the console, but it's value is not changing no matter how many times I click a different option. But when I make some changes in the code editor and save it it display the new value but if I try againg changing the value clicking on it it does not change.
const quantityChange = (e) => {
console.log('changing quantity')
const newCart = shoppingCart
console.log(
"new cart variable created. It's value is " + JSON.stringify(newCart)
)
const index = e.target.dataset.index
console.log('Item index is ' + index)
newCart[index]['quantity'] = e.target.value
console.log(
'The new quantity for the item is ' + newCart[index]['quantity']
)
setShoppingCart(newCart)
console.log(
'new value set for shopping cart. ' +
JSON.stringify(shoppingCart)
)
}
const ShoppingCart = ({ shoppingCart, quantityChange, removeFromCart }) => {
const onSubmit = (e) => {
e.preventDefault()
}
if (shoppingCart.length === 0) {
return <div>There's no items in the shopping cart</div>
}
const options = []
for (let i = 0; i <= 10; i++) {
options.push(
<option name="quantity-1" value={i} key={'qsfkajlf' + i}>
{i}
</option>
)
}
return (
<div>
<h1>
There's {shoppingCart.length} item{shoppingCart.length > 1 && 's'} in
your shopping cart.
</h1>
{shoppingCart.map((item, index) => {
const { id, name, price, image, quantity } = item
return (
<div key={id}>
<div className="flex-container">
<div className="img-container">
<img src={image} alt="" />
</div>
<div className="item-info">
<h2>{name}</h2>
<p>${price}</p>
</div>
<div className="remove-quantity">
<div className="quantity">
<form onSubmit={onSubmit}>
{/* TODO: don't allow more than 10 items */}
<select
id="quantity"
data-index={index}
onChange={quantityChange}
value={quantity}
>
{options}
</select>
</form>
</div>
<div className="remove-container">
<button data-remove={index} onClick={removeFromCart}>
x
</button>
</div>
</div>
</div>
</div>
)
})}
</div>
)
}
It isn't updating because you are doing this:
const quantityChange = (e) => {
console.log('changing quantity')
const newCart = shoppingCart // <-- copy a reference to shoppingCart
// do stuff
setShoppingCart(newCart) // <-- write the original (modified) object back in
}
As the object reference doesn't change, the re-render isn't triggered.
Instead, you just need to copy your object (instead of just copying the reference):
const quantityChange = (e) => {
console.log('changing quantity')
const newCart = {...shoppingCart} // <-- copy the contents into a new object

Categories