Why my functional component doesn't re-render when change state? (react) - javascript

I'm stuck in project because my functional component do not re-render when I update this state.
const OneProduct = (props) => {
return (
<div className='productincart'>
<div className='name'>{props.name}</div>
<div className='price'>Price: {props.price}</div>
<div className='quantity'>{props.quantity}</div>
</div>)
}
const InsideCart = (props) => {
const cartTemporary = props.sendCart
const [cart, setCart] = useState([])
useEffect(() => {
setCart(cartTemporary)
}, [cartTemporary])
const showCart = () => {
console.log(cart)
}
return (
<div className='insidecart'>
{cart.map(product => <OneProduct name={product.name} price=
{product.price} quantity={product.quantity} key={uniqid()} />)}
<button onClick={showCart}>SHOW CART</button>
</div>
)
}
In code above, I putting products from shop to the cart, next I want to display it on the screen. When I'm putting new item to the cart (new object in cart array), everything works fine. When I'm only update quantity of object already exist in cart array, nothing happens. After quantity updating, cart is updated with new value, but it doesn't causing re-render of component.
That is problem to solve, how to make component re-render after quantity value update.
Below format of objects which I add to cart, also added mechanism of adding and increasing quantity.
const Products = (props) => {
const initialProducts = [
{ id: 0, name: "Fish - Betta splendens - Plakat Koi - Siamese fighting fish", image: image0, price: 23.17, quantity: 1 },
{ id: 1, name: "Fish - Betta splendens - Plakat - Siamese fighting fish", image: image1, price: 10.12, quantity: 1 },
{ id: 2, name: "Fish - Phenacogrammus interruptus - Congo tetra", image: image2, price: 5.19, quantity: 1 },
{ id: 3, name: "Fish - Thayeria boehlkei - Pinguin tetra", image: image3, price: 1.31, quantity: 1 },
{ id: 4, name: "Fish - Pterophyllum scalare - Angelfish", image: image4, price: 5.77, quantity: 1 },
{ id: 5, name: "Fish - Boeseman's rainbowfish Melanotaenia - Boesemani", image: image5, price: 4.90, quantity: 1 },
{ id: 6, name: "Fish - Ram cichlid - Mikrogeophagus ramirezi", image: image6, price: 4.61, quantity: 1 },
{ id: 7, name: "Fish - Corydoras aeneus sp.black venezuela", image: image7, price: 2.87, quantity: 1 },
]
const [products, setProducts] = useState(initialProducts)
const [toCart, setToCart] = useState([])
const getProductId = (e) => {
const idString = e.target.dataset.id
const id = Number(idString)
return id
}
const getProductById = (e) => {
const id = getProductId(e)
const itemFound = products.find(product => product.id === id)
const foundedOrNot = toCart.find(product => product === itemFound)
if (foundedOrNot === undefined) {
setToCart([...toCart, itemFound])
}
else {
const index = toCart.findIndex(product => product === itemFound)
toCart[index].quantity = toCart[index].quantity + 1
}
}
return (
<div className="products">
<Nav sendToCart={toCart} />
<InsideCart sendCart={toCart} />
<div className='container'>{products.map((product) => <Product onClick={getProductById} title={product.name} img={product.image} price={product.price} id={product.id} key={product.id} />)}</div>
</div>
);
};
Also add simple component which determines rendering what is inside the cart.
const OneProduct = (props) => {
return (<div className='productincart'><div className='name'>{props.name}</div> <div className='price'>Price: {props.price}</div><div className='quantity'>{props.quantity}</div></div>)
}

I already solved the problem. Issue was not using setToCart in product component. Thank you guys for all advices!
Updated code below:
const getProductById = (e) => {
const id = getProductId(e)
const itemFound = products.find(product => product.id === id)
const foundedOrNot = toCart.some(product => product === itemFound)
if (foundedOrNot === false) {
setToCart([...toCart, itemFound])
}
else {
const index = toCart.findIndex(product => product === itemFound)
const newCart = toCart.map(cart => {
if (cart.id === index) {
cart.quantity = cart.quantity + 1
return cart
}
return cart
})
setToCart(newCart)
}
}

Related

Set acitve classname to multiple items in react js (map) and remove

I need to set the active classname to multiple onclick items inside a .map
I need the list of active items that were clicked
The items that were clicked will be highlighted in yellow, and when i click the same item again it should be removed from active list items.
const [data, setData] = useState([]);
const [activeIndicies, setActiveIndicies] = useState(() =>
data?.map(() => false)
);
useEffect(() => {
// This data is coming from the API response
const data = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
];
setData(data);
}, []);
return statement
onClick={() => {
setActiveIndicies(
activeIndicies.map((bool, j) => (j === index ? true : bool))
);
}}
Code Sandbox
Thank you.
try this one:
import "./styles.css";
import React, { useState, useEffect } from "react";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "one", active: false },
{ id: 2, name: "two", active: false },
{ id: 3, name: "three", active: false }
]);
return (
<div className="App">
<h2>Set active className to multiple items on .map</h2>
{data?.map((item, index) => {
return (
<p className={data[index].active ? "selected" : "notselected"}
onClick={() => {
setData((prevState) =>
_.orderBy(
[
...prevState.filter((row) => row.id !== item.id),
{ ...item, active: !item.active }
],
["name"],
["asc"]
)
);
}}
>
{item.name}
</p>
);
})}
</div>
);
}
You can acheive this by simply making some minor changes to your code:
// Changing the state value to an object so that it can
// store the active value for exact item ids
const [activeIndicies, setActiveIndicies] = useState({});
Then inside of .map()
....
// Checking if there is any value for the item id which is being mapped right now.
const selected = activeIndicies[item.id];
return (
<p
className={selected ? "selected" : "notselected"}
onClick={() => {
/* Then setting the state like below where it toggles
the value for particular item id. This way if item is
selected it will be deselected and vice-versa.
*/
setActiveIndicies((prevState) => {
const newStateValue = !prevState[item.id];
return { ...prevState, [item.id]: newStateValue };
});
}}
// Key is important :)
key={item.id}
>
{item.name}
</p>
);
Hello, friends!
I solved this problem in a more convenient way for me )
const data = [
{ id: 1, name: "Ann", selected: true },
{ id: 2, name: "Serg", selected: false },
{ id: 3, name: "Boris", selected: true },
];
//you don't even need to have a boolean field in the object -
//it will be added by itself on the first click on the element
// const data = [{ name:"Ann", id:1}, { name:"Serg", id:2 },{ name:"Boris", id:3 },]
const [users, setUsers] = useState(data); // no square brackets-[data]
function handleActive(item) {
setUsers((prev) => {
return prev.map((itemName) => {
if (itemName.name === item.name) {
return { ...itemName, selected: !itemName.selected };
// itemName.selected = !itemName.selected // or so
}
return itemName;
});
});
}
return (
{
users.map((item, i) => {
// let current = item.selected === true
// or so -> className={current === true ? 'users': ''}
return (
<div
onClick={() => handleActive(item)}
className={item.selected === true ? "active" : ""}
key={i}
>
{item.name}
</div>
);
})
}
);

Change a single product quantity using state (react)

So ,I wanted to change this one product object in the UI with a button click, and reduce the quantity of the product. But the quantity amount did not change .What is the best move in situations like that.
let product={ a:1,b:2,c:3};
Lets say changing the value.
b:5;
please check this sample code
import React, { useState, useEffect } from 'react';
export default function App() {
const [products, setProducts] = useState([
{ name: 'Product 1', qty: 1, price: 10 },
{ name: 'Product 2', qty: 1, price: 20 },
]);
const [totalAmt, setTotalAmt] = useState(0);
useEffect(() => {
const total = products.reduce((s, i) => s += i.price * i.qty, 0);
setTotalAmt(total);
}, [products]);
const incrQty = (index) => {
setProducts((v) => {
const a = JSON.parse(JSON.stringify(v)); // FOR AVOIDING MUTATING DATA, BECAUSE IN STRICT MODE THIS SETSTATE CALLBACK WILL TRIGGER TWICE
a[index].qty += 1;
return [...a];
});
};
const decrQty = (index) => {
setProducts((v) => {
const a = JSON.parse(JSON.stringify(v));
a[index].qty -= 1;
return [...a];
});
};
return (
<div>
Total Amount: Rs.{totalAmt}
<ul>
{products.map((e, k) => (
<li key={k}>
{e.name}, qty={e.qty}, subtotal {e.qty * e.price},
<button onClick={e => incrQty(k)}>Incr Qty</button>
{
(e.qty > 1)?<button onClick={e => decrQty(k)}>Decr Qty</button>:null
}
</li>
))}
</ul>
</div>
);
}

React performance issues caused by rerendering

I have an orders component that contains some orders each order has products and the user can update the quantity and price the problem is that the update process is very slow because if I update a product quantity for example all products in the order get remounted again and I think this is the main issue. if I have 100 products the product page render 100 times or more (one time for each product ) here is my current implementation on code sandbox: https://codesandbox.io/s/holy-tdd-3nj7g?file=/src/OrderingPage/Order/index.js
here is the order component that have multiple order but for simplicity lets assume we only have one order
import { useState, useCallback } from "react";
import Order from "./Order/index";
const OrderingScreen = () => {
const initialOrderData = {
order: {
total: 0,
vat: 0,
deliveryCharge: 0,
date: 0,
orderStart: 0,
orderEnd: 0,
customerGender: "",
actualPaid: 0,
dateStr: "",
payType: "cash",
itemsCount: 0,
orderDetails: [
{ name: "prod1", sellPrice: 120, quantity: 3 },
{ name: "prod2", sellPrice: 12, quantity: 2 },
{ name: "prod3", sellPrice: 1123, quantity: 2 },
{ name: "prod4", sellPrice: 1543, quantity: 1 },
{ name: "prod5", sellPrice: 123, quantity: 8 }
]
}
//other properties
};
const [ordersData, setOrdersData] = useState([initialOrderData]);
const resetOrder = useCallback(() => {
let ordersDataCopy = [...ordersData];
ordersDataCopy[0] = initialOrderData;
setOrdersData(ordersDataCopy);
}, [ordersData]);
const updateOrderProducts = useCallback(
(products) => {
let ordersCopy = [...ordersData];
ordersCopy[0]["order"]["orderDetails"] = [...products];
setOrdersData(ordersCopy);
},
[ordersData]
);
const updateOrder = useCallback(
(order) => {
let ordersCopy = [...ordersData];
ordersCopy[0]["order"] = { ...order };
setOrdersData(ordersCopy);
},
[ordersData]
);
return (
<Order
order={ordersData[0].order}
products={ordersData[0].order.orderDetails}
updateOrderProducts={updateOrderProducts}
updateOrder={updateOrder}
resetOrder={resetOrder}
/>
);
};
export default OrderingScreen;
here is the single order component
import OrderItem from "./OrderItem";
import { useEffect, memo, useCallback } from "react";
const Order = ({ order, products, updateOrderProducts, updateOrder }) => {
const handleOrderChange = useCallback((propertyName, value) => {
let orderCopy = { ...order };
orderCopy[propertyName] = value;
updateOrder(orderCopy);
});
const deleteProduct = useCallback((index) => {
let productsCopy = [...products];
productsCopy = productsCopy.filter(
(product) => product !== productsCopy[index]
);
updateOrderProducts(productsCopy);
}, []);
const handleOrderItemRemove = useCallback((index) => {
deleteProduct(index);
}, []);
const handleQuantityChange = useCallback((index, quantity) => {
let productsCopy = [...products];
productsCopy[index]["quantity"] = quantity;
updateOrderProducts(productsCopy);
}, []);
return (
<div className="d-flex px-2 flex-grow-1 mb-1">
{products.map((product, idx) => (
<OrderItem
product={product}
key={idx}
index={idx}
onRemove={handleOrderItemRemove}
onQuantityChange={handleQuantityChange}
updateProduct={handleOrderChange}
/>
))}
</div>
);
};
export default memo(Order);
and the last component which is the product component which I think is causing the performance issue (it render 1 + the number of products in the order if I update the quantity of one product )
import RemoveCircleIcon from "#mui/icons-material/RemoveCircle";
import AddCircleIcon from "#mui/icons-material/AddCircle";
import { memo, useMemo, useState, useEffect } from "react";
const OrderItem = ({ product, index, onQuantityChange }) => {
console.log("remount");
const [itemQuantity, setItemQuantity] = useState(product.quantity);
const incrementQuantity = () => {
onQuantityChange(index, itemQuantity + 1);
};
const decrementQuantity = () => {
itemQuantity > 1 && onQuantityChange(index, itemQuantity - 1);
};
useEffect(() => {
setItemQuantity(product.quantity);
}, [product.quantity]);
const productInfo = useMemo(() => (price, quantity, name) => {
let total = price * quantity;
total = +total.toFixed(2);
price = +price.toFixed(2);
return (
<div className={`col-9 col-xl-10 border rounded-start p-1 `}>
{name}
<div className="justify-content-around d-flex">
{"Price:" + price}
{" Quantity:" + quantity}
{" Total:" + total}
</div>
</div>
);
});
useEffect(() => {
setItemQuantity(product.quantity);
}, [product]);
const quantityColumn = (
<div>
<AddCircleIcon onClick={incrementQuantity} />
{itemQuantity}
<RemoveCircleIcon onClick={decrementQuantity} />
</div>
);
return (
<div style={{ marginBottom: "25px" }}>
{productInfo(product.sellPrice, product.quantity, product.name)}
{quantityColumn}
</div>
);
};
export default memo(OrderItem);
what I want to achieve is a snappy component update (maybe by making the product component mount only for the changed product)
you may see it fast on the sandbox but this version just explains the problem only... the real version is much complicated
You can improve performance by changing your React.memo components.
Instead of memo(OrderItem) pass as second argument function that will compare previous and current state:
function areEqualOrderItem(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
return prevProps.quantity === nextProps.quantity;
}
export default memo(OrderItem, areEqualOrderItem);
Also I suggest do not use array index as component key try product name instead of this.
useCallback do nothing in your code. Instead you can use this one:
const handleOrderItemRemove = useCallback((index) => {
updateOrderProducts(prod => {
let productsCopy = [...prod];
productsCopy = productsCopy.filter(
(product) => product !== productsCopy[index]
);
return productsCopy;
});
}, [updateOrderProducts]);
const updateOrderProducts = useCallback(
(products) => {
setOrdersData(ords => {
let ordersCopy = [...ords];
ords[0]["order"]["orderDetails"] = [...products];
return ordersCopy;
});
},
[setOrdersData]
);
When you fix all your callbacks you can boost performance. At this time, your code cause rerender of all items almost every small change.

Updating useState without inserting another object

This is the full code of the component, I am so tired that I can't think much on how to solve this problem
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
function CnContent(props) {
const getCart = JSON.parse(localStorage.getItem('cart'));
const products = [
{
id: 0,
imgsrc: process.env.PUBLIC_URL + '/assets/images/products/grocery-staples/pro_1.jpg',
name: "O'range 500 ml Coconut Oil(Bottle)",
quantity: "500 ml",
offer_price: "₹ 116",
real_price: "₹ 219",
inCart: 1
},
{
id: 1,
imgsrc: process.env.PUBLIC_URL + '/assets/images/products/grocery-staples/pro_2.jpg',
name: "Parachute 100% Pure Coconut Oil (Plastic Bottle)",
quantity: "600 ml",
offer_price: "₹ 210",
real_price: "₹ 250",
inCart: 1
},
{
id: 2,
imgsrc: process.env.PUBLIC_URL + '/assets/images/products/grocery-staples/pro_3.jpg',
name: "Fortune Soya Chunks",
quantity: "1 kg",
offer_price: "₹ 126",
real_price: "₹ 135",
inCart: 1
},
]
const [cart, setCart] = useState(getCart);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart])
function addToCart(pr) {
let duplicate = cart.find((x) => x.id === pr.id)
if (!duplicate) {
setCart((prevState) => {
return [...prevState, pr]
});
}
else {
setCart((prevState) => [...prevState, prevState[pr.id] = { inCart: prevState[pr.id].inCart + 1 }]); // This creates another object and sends to the array (cart), but I want to change the value of the specific product(cart[pr.id]).
}
}
return (
<>
<div className='cn-table'>
<div className='section-left'></div>
<div className='section-right'>
{
products.map((pr, i) => {
return (
<div tabIndex='-1' className='products' key={i}>
<div className="products__wrapper">
<Link to='https://www.youtube.com' className='products__link'>
<div className='products__img'>
<img src={pr.imgsrc} alt={pr.name}></img>
</div>
<div className='products__name'>{pr.name}</div>
<div className='products__quantity'>{pr.quantity}</div>
</Link>
<div className='products__details'>
<div className='products__details__price'>
<div className='products__details__price__offer'>{pr.offer_price}</div>
<div className='products__details__price__real'>{pr.real_price}</div>
</div>
<div className='add-to-cart zero-products'>
<button className='add-to-cart__remove' >-</button>
<button className='add-to-cart__text active'>{pr.inCart}</button>
<button className='add-to-cart__add' onClick={() => addToCart(pr)}>+</button>
</div>
</div>
</div>
</div>
)
})
}
</div>
</div>
</>
)
}
export { CnContent };
These are results of localStorage value, the problem is instead of changing the value of inCart of the specific object, it's creating a new object with property "inCart".
0: {id: 0, pr_category: "grocery-staples", imgsrc: "/assets/images/products/grocery-staples/pro_1.jpg",…}
1: {id: 1, pr_category: "grocery-staples", imgsrc: "/assets/images/products/grocery-staples/pro_2.jpg",…}
2: {inCart: 1} /*Instead of this new object I want to change the value in the upper objects*/
Any suggestions will be appreciated. Thank you !
EDIT: Changed the value of inCart from 0 to 1.
You need to map over the cart and update only the product you want
setCart((prevState) => prevState.map(product=>{
if (product.id === pr.id) {
return {
...product,
inCart: product.inCart + 1
}
}
return product;
});
The problem is solved this code if I change the value of inCart from 0 to 1, but if possible post an answer where I don't have change the value of inCart.
function addToCart(pr) {
setCart((prevState) => {
let duplicate = prevState.find((x) => x.id === pr.id)
if (!duplicate)
return [...prevState, pr];
else {
prevState[pr.id].inCart += 1;
return [...prevState];
}
})
}
I have use this code above instead of:
function addToCart(pr) {
let duplicate = cart.find((x) => x.id === pr.id)
if (!duplicate) {
setCart((prevState) => {
return [...prevState, pr]
});
}
else {
setCart((prevState) => [...prevState, prevState[pr.id] = { inCart: prevState[pr.id].inCart + 1 }]); // This creates another object and sends to the array (cart), but I want to change the value of the specific product(cart[pr.id]).
}
}

React: How to Pass State on Each Mapped Array Items?

I rendered a list of buttons using Array.map method in a function component. When I tried to pass state to each mapped array items, the rendered results changed all array items at once, instead of one by one.
Here is my code. Am I doing something wrong? Sorry if the question has been solved in other thread or I used the wrong method. This is my first React project and I am still learning. It would be very appreciated if someone could advise. Thank you!
import React, { useState } from "react"
export default function Comp() {
const [isActive, setActive] = useState(false)
const clickHandler = () => {
setActive(!isActive)
console.log(isActive)
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={clickHandler}>
{item.name} {isActive ? "active" : "not active"}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Put the individual item into a different component so that each has its own active state:
export default function Comp() {
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => (
items.map(item => <Item key={item.id} name={item.name} />)
);
return (
<ul>{renderList(data)}</ul>
)
}
const Item = ({ name }) => {
const [isActive, setActive] = useState(false);
const clickHandler = () => {
setActive(!isActive);
};
return (
<li>
<button onClick={clickHandler}>
{name} {isActive ? "active" : "not active"}
</button>
</li>
);
};
You need to set the active-id in handling the click-event. That will in-turn render active/non-active conditionally:
Notice the flow (1) > (2) > (3)
function Comp() {
const [activeId, setActiveId] = React.useState(null);
const clickHandler = (item) => {
setActiveId(item.id) // (2) click-handler will set the active id
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={() => clickHandler(item)}> // (1) passing the clicked-item so that we can set the active-id
{item.name} {item.id === activeId ?
"active" : "not active" // (3) conditionally render
}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Good Luck...

Categories