React performance issues caused by rerendering - javascript

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.

Related

how to calculate the total price of your cart

I was creating a shopping cart using react and redux and everything works perfectly without any errors I'm wondering how do I calculate the total price by adding up all the items in the cart
my cart-slice file
import { createSlice } from '#reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
totalQuantity: 0,
},
reducers: {
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.title,
});
} else {
existingItem.quantity++;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
},
removeItemFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find(item => item.id === id);
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter(item => item.id !== id);
} else {
existingItem.quantity--;
existingItem.totalPrice = existingItem.totalPrice - existingItem.price;
}
},
},
});
export const cartActions = cartSlice.actions;
export default cartSlice;
the cart.js file
import { useSelector } from 'react-redux';
import Card from '../UI/Card';
import classes from './Cart.module.css';
import CartItem from './CartItem';
const Cart = (props) => {
const cartItems = useSelector((state) => state.cart.items);
return (
<Card className={classes.cart}>
<h2>Your Shopping Cart</h2>
<ul>
{cartItems.map((item) => (
<CartItem
key={item.id}
item={{
id: item.id,
title: item.name,
quantity: item.quantity,
total: item.totalPrice,
price: item.price,
}}
/>
))}
</ul>
</Card>
);
};
export default Cart;
the cart item file
import { useDispatch } from 'react-redux';
import classes from './CartItem.module.css';
import { cartActions } from '../../store/cart-slice';
import { useState } from 'react';
const CartItem = (props) => {
const dispatch = useDispatch();
const { title, quantity, total, price, id } = props.item;
const removeItemHandler = (e) => {
dispatch(cartActions.removeItemFromCart(id));
};
const addItemHandler = () => {
dispatch(
cartActions.addItemToCart({
id,
title,
price,
})
);
};
return (
<>
<li className={classes.item}>
<header>
<h3>{title}</h3>
<div className={classes.price}>
${total.toFixed(2)}{' '}
<span className={classes.itemprice}>(${price.toFixed(2)}/item)</span>
</div>
</header>
<div className={classes.details}>
<div className={classes.quantity}>
x <span>{quantity}</span>
</div>
<div className={classes.actions}>
<button onClick={removeItemHandler}>-</button>
<button onClick={addItemHandler}>+</button>
</div>
</div>
</li>
</>
);
};
export default CartItem;
can somebody please help me on how do we calculate the total price of the cart?
You can either do it in your reducers (addItemToCart, removeItemFromCart), or add another reducer with action that does that for you in a side effect.
Watching your implementation I see that you are mutating your state which is an anti-pattern. Redux is way safer to use and easier to manage with immutable state.
So instead of
state.totalQuantity++;
it is way better to return a new state object
{ ...state, totalQuantity: state.totalQuantity }
I don't know exactly whether redux/toolkit already copies the state internally, but you should not take that as given. Always return a new state.
Now to your Question (I dont know how your Item model looks like):
const totalPrice = state.items
.map(everyItem => {
return everyItem.quantity * everyItem.price;
})
.reduce((totalPrice, singleItemPrice) => totalPrice + singleItemPrice, 0);
Key takeaway here should be https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
This would be a perfect use case for a Memoized Selector. Since you have state with a items array in it you can create a selector to reduces this state to a single number. For example:
const selectCart = state => state.cart
const selectTotalPrice = createSelector([selectCart], (cart) => {
return cart.items.reduce((total, item) => (item.price * quantity) + total, 0);
})

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>
);
}

Convert class component to functional component in React

I'm learning React hooks so in order to do that I'm trying to convert a class component to a functional component but I still get some errors.
Here is the original working component written as a class:
import React, { Component } from 'react';
import NavBar from './components/navbar';
import Counters from './components/counters';
class App extends Component {
state = {
counters: [
{ id: 0, value: 5 },
{ id: 1, value: 1 },
{ id: 2, value: 2 },
],
};
handleDelete = (counterId) => {
const counters = this.state.counters.filter((c) => c.id !== counterId);
this.setState({ counters });
};
handleReset = () => {
const counters = this.state.counters.map((c) => {
c.value = 0;
return c;
});
this.setState({ counters });
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
render() {
return (
<React.Fragment>
<NavBar
totalCounters={this.state.counters.filter((c) => c.value > 0).length}
/>
<main className='container'>
<Counters
counters={this.state.counters}
onReset={this.handleReset}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}
/>
</main>
</React.Fragment>
);
}
}
export default App;
And this is the converted version which uses hooks.
import React, { useState } from 'react';
import NavBar from './components/navbar';
import Counters from './components/counters';
const App = () => {
const [counters, setCounters] = useState([
{ id: 0, value: 5 },
{ id: 1, value: 1 },
{ id: 2, value: 2 },
]);
const handleDelete = (counterId) => {
const counterss = counters.filter((c) => c.id !== counterId);
setCounters({ counterss });
};
const handleReset = () => {
const counterss = counters.map((c) => {
c.value = 0;
return c;
});
setCounters({ counterss });
};
const handleIncrement = (counter) => {
const counterss = [...counters];
const index = counterss.indexOf(counter);
counterss[index] = { ...counter };
counterss[index].value++;
setCounters({ counterss });
};
return (
<React.Fragment>
<NavBar totalCounters={counters.filter((c) => c.value > 0).length} />
<main className='container'>
<Counters
counters={counters}
onReset={handleReset}
onDelete={handleDelete}
onIncrement={handleIncrement}
/>
</main>
</React.Fragment>
);
};
export default App;
Most of it works fine but it keeps throwing an error saying that filter is not a function. Here it is the message:
TypeError: counters.filter is not a function
The main culprit appears to be the way you are updating your state, which is like this:
setCounters({ counterss });
This will actually set your counters state to an object with the property counterss, so your state will contain the following:
{ counterss: [/* rest of the array */] }
The exact error being thrown is referring to the fact that you are attempting to call .filter on an object instead of an array. To fix this issue you should simply update your state like this:
setCounters(counterss);
setCounters({ counterss })
should be
setCounters(counterss)
It throws an error because you set the new state as an object setCounters({ counterss });. But you want an array: setCounters(counterss);. This way it won't throw an error the second time setCounters is called.

React Hooks: Shuffle array immediately on load, and onClick

I have an array that I am trying to shuffle on initial load, and onClick. My functions seem to be working but aren't visible unless there is a page rerender.
2 issues I'm trying to resolve:
I want to shuffle on initial load, but the shuffle doesn't occur unless there is a rerender.
I want to shuffle on button press, but the shuffle doesn't occur unless there is a rerender.
Here is my CodeSandbox
Thanks
import React, {
useEffect,
useState
} from "react";
const myArray = [{
name: "cat",
count: 0
},
{
name: "dog",
count: 0
},
{
name: "hamster",
count: 0
},
{
name: "lizard",
count: 0
}
];
function shuffle(arra1) {
var ctr = arra1.length,
temp,
index;
while (ctr > 0) {
index = Math.floor(Math.random() * ctr);
ctr--;
temp = arra1[ctr];
arra1[ctr] = arra1[index];
arra1[index] = temp;
}
return arra1;
}
function App(props) {
const [list, setList] = useState(myArray);
useEffect(() => {
const mountArray = shuffle(myArray);
setList(mountArray);
}, []);
function handleShuffle() {
const changes = shuffle([...list]);
setList(changes);
console.log("Shuffle", myArray, changes);
}
return (
<div>
{list.map((x, index) => (
<div key = {x.name + x.index}>
{x.name} - {x.count}
<button onClick={() => setList([...list], (x.count = x.count + 1))}>
+
</button>
</div>
))}
<button onClick={handleShuffle}>
Shuffle
</button>
</div>
);
}
export default App;
HAI i have made some changes in App.js
import React, { useEffect, useState } from "react";
const myArray = [
{ name: "cat", count: 0 },
{ name: "dog", count: 0 },
{ name: "hamster", count: 0 },
{ name: "lizard", count: 0 }
];
function shuffle(arra1) {
var ctr = arra1.length,
temp,
index;
while (ctr > 0) {
index = Math.floor(Math.random() * ctr);
ctr--;
temp = arra1[ctr];
arra1[ctr] = arra1[index];
arra1[index] = temp;
}
return arra1;
}
function App(props) {
const [list, setList] = useState(myArray);
useEffect(() => {
const mountArray = shuffle(myArray);
setList(mountArray);
}, []);
function handleShuffle() {
const changes = shuffle([...list]);
setList(changes);
console.log("Shuffle", myArray);
}
return (
<div>
{list.map((x, index) => (
<div key={x.name + x.index}>
{x.name} - {x.count}
<button onClick={() => setList([...list], (x.count = x.count + 1))}>
+
</button>
</div>
))}
<button onClick={handleShuffle}>Shuffle</button>
</div>
);
}
export default App;
Your setList function is used to modify the array and returns a new array, with shuffled values, so you need to apply that function non initial rendering.
useEffect(() => {
setList(shuffle(myArray))
}, []);
Html changes only if the state has changed, so make some state inside a component, and update it every time you want to update the html.
function App(props){
const [myArray, setMyArray] = useState([])
// rest of the code
}

React hooks: Parent component not re-rendering

I'm trying to update the state of a parent component from a child using a callback. The state and call back are passed to a text input. The callback is being called, the state of the parent is changed, but it doesn't rerender. The value of the input field stays the same. If force rendering is used, the text field updates every time a new character is added (As desired). I'm not sure what could be causing this, from my understanding the setState hooks provided are supposed to rerender unless the state is unchanged.
EDIT: (Added the parent component not just the callback)
Below is the parent component
import Card from './Card'
import Instructions from './instructions'
import Title from './title'
import React, { useRef, useState, useCallback, useEffect } from 'react'
import { DropTarget } from 'react-dnd'
import ItemTypes from './ItemTypes'
import update from 'immutability-helper'
const Container = ({ connectDropTarget }) => {
const ref = useRef(null)
const titleRef = useRef()
const instructionsRef = useRef()
const appRef = useRef()
useEffect(() => {
// add when mounted
document.addEventListener("mousedown", handleClick);
// return function to be called when unmounted
return () => { document.removeEventListener("mousedown", handleClick);};
}, []);
const handleClick = e => {
if (titleRef.current.contains(e.target)) {
setFocus("Title");
return;
} // outside click
else if(instructionsRef.current.contains(e.target)){
setFocus("Instructions");
return;
}
setFocus(null);
};
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const [focus,setFocus] = useState(null);
const [title, setTitle] = useState({id: "Title", text: "Default",type: "Title", data:[]});
const [instructions, setInstructions] = useState({id: "Instructions",type:"Instructions", text: "Instructions", data:[]});
const [cards, setCards] = useState([
{
id: 1,
text: 'Write a cool JS library',
},
{
id: 2,
text: 'Make it generic enough',
},
{
id: 3,
text: 'Write README',
},
{
id: 4,
text: 'Create some examples',
},
{
id: 5,
text: 'Spam in Twitter and IRC to promote it',
},
{
id: 6,
text: '???',
},
{
id: 7,
text: 'PROFIT',
},
])
const moveCard = useCallback(
(id, atIndex) => {
const { card, index } = findCard(id)
setCards(
update(cards, {
$splice: [[index, 1], [atIndex, 0, card]],
}),
)
},
[cards],
)
const findCard = useCallback(
id => {
const card = cards.filter(c => `${c.id}` === id)[0]
return {
card,
index: cards.indexOf(card),
}
},
[cards],
)
const updateItem = useCallback(
(id,field,additionalData,value) => {
return;
},
[cards], //WHat does this do?
)
const updateTitle = text => {
console.log("Updating title")
let tempTitle = title;
tempTitle['text'] = text;
//console.log(text);
//console.log(title);
//console.log(tempTitle);
setTitle(tempTitle);
//console.log(title);
//console.log("done");
forceUpdate(null);
}
connectDropTarget(ref)
return (
<div ref={appRef}>
<div ref={titleRef} >
<Title item={title} focus={focus} updateFunction={updateTitle}/>
</div>
<div ref={instructionsRef} >
<Instructions item={instructions} focus={focus}/>
</div>
<div className="Card-list" ref={ref}>
{cards.map(card => (
<Card
key={card.id}
id={`${card.id}`}
text={card.text}
moveCard={moveCard}
findCard={findCard}
item={card}
focus={focus}
/>
))}
</div>
</div>
)
}
export default DropTarget(ItemTypes.CARD, {}, connect => ({
connectDropTarget: connect.dropTarget(),
}))(Container)
The code of the component calling this function is:
import React from 'react'
function Title(props) {
if(props.focus === "Title")
return(
<input
id="Title"
class="Title"
type="text"
value={props.item['text']}
onChange = { e => props.updateFunction(e.target.value)}
/>
);
else
return (
<h1> {props.item['text']} </h1>
);
}
export default Title
The problem is here
const updateTitle = text => {
let tempTitle = title; // These two variables are the same object
tempTitle['text'] = text;
setTitle(tempTitle); // problem is here
}
React uses the object.is() method to compare two values before and after. Look at this
Object.is(title, tempTitle) // true
You should make "title" and "tempTitle" different objects, like this
const updateTitle = text => {
let tempTitle = {...title}; // tempTitle is a new object
tempTitle['text'] = text;
setTitle(tempTitle);
}
And this is a demo of mutable object.
var a= {name:1}
var b = a;
b.name=2
var result = Object.is(a,b)
console.log(result)
// true

Categories