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
Related
i was doing a todo list app on React, and, tring to handle the changes i got the following error:
Uncaught TypeError: prevTodos is not iterable
My handle function:
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
Full Code:
import React, { useState, useRef, useEffect } from "react";
import TodoList from "./TodoList";
import { v4 } from "uuid";
const LOCAL_STORAGE_KEY = 'todoApp.todos';
function App() {
const [todos, setTodos] = useState([]);
const todoNameRef = useRef();
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
return (
<>
<TodoList todos={todos} />
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Complete</button>
<div>0 left to do</div>
</>
);
}
export default App;
This lines are the problem
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
you should parse the value before setting and there is no need for that setTodos() without value, because of that you would later get "undefined" is not a valid JSON:
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(JSON.parse(storedTodos));
}, []);
I haven't ran the code or tested anything but I think it's just the way your calling setTodos. Instead of passing in an anonymous function I would define the new todo list first, then use it to set the state. Try something like this.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
const newTodos = [...prevTodos, { id: v4(), name: name, complete: false }];
setTodos(newTodos);
todoNameRef.current.value = null;
}
You could probably get away with this too.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos([...prevTodos, { id: v4(), name: name, complete: false }]);
todoNameRef.current.value = null;
}
Hopefully that helps.
I try to build sliders with different categories that each user has his point.
The informant comes from the json server
What I need I do not succeed in having the customer choose a user that is numbered and the dot will be colored in the slider How do I do that?
In addition he has the option to delete and return the point.
I was able to delete the points by deleting them in the object. But I could not return, is there a possibility to return?
Broker.jsx
import React, { useEffect, useState } from 'react';
import './style.css';
import Combo from '../components/Combo/Combo';
import Sliders from '../components/Sliders/Sliders';
const GetUsersDataFromManipulation = (users, field) => {
const state = users.reduce((store, user) => {
const userId = user.user
const currentManipulationUserData = user.profileManipulation[field]
if (currentManipulationUserData.length === 0) {
return store
}
store[userId] = currentManipulationUserData[0].bid
return store;
}, {})
return state;
};
function Broker({ manipulations }) {
const users = manipulations[2].users
const [hiddenUser, setHiddenUser] = useState(() => {
const visible = {};
for (let user of users) {
visible[user.user] = true;
}
return visible;
})
const GetUsersBid = (profileManipulation) => {
const data = GetUsersDataFromManipulation(users, `${profileManipulation}`); if (!Object.keys(data).length) {
return null
}
return data;
};
const gender = GetUsersBid('gender');
const age = GetUsersBid('age');
const marital = GetUsersBid('marital');
const children = GetUsersBid('children');
const education = GetUsersBid('education');
const interests = GetUsersBid('interests');
const dynamicInterests = GetUsersBid('dynamicInterests');
const showUser = (user_id) => {
const new_hidden = { ...hiddenUser }
new_hidden[user_id] = true;
setHiddenUser(new_hidden);
}
const hideUser = (user_id) => {
const new_hidden = { ...hiddenUser }
console.log(user_id)
new_hidden[user_id] = false;
setHiddenUser(new_hidden);
}
const [userInformation, setUserInformation] = useState([
{ name: 'gender', bids: gender },
{ name: 'age', bids: age },
{ name: 'marital', bids: marital },
{ name: 'children', bids: children },
{ name: 'education', bids: education },
{ name: 'interests', bids: interests },
{ name: 'dynamicInterests ', bids: dynamicInterests },
]);
useEffect(() => {
const curret_User_Info = [...userInformation]
for (let user of Object.keys(hiddenUser)) {
for (let i = 0; i < curret_User_Info.length; i++) {
if (curret_User_Info[i].bids !== null) {
if (hiddenUser[user] === false) {
delete curret_User_Info[i].bids[user]
}
else {
//What am I returning here? So that the bids will return?
}
}
}
}
setUserInformation(curret_User_Info)
}, [hiddenUser])
return (
<div>
<div className="button" >
{userInformation && <Combo users={users} showUser={showUser} hideUser={hideUser} userInformation={userInformation} />}
</div>
<div className='slid'>
{userInformation.map(sliderDetails => {
return (
<div className={sliderDetails.name} key={sliderDetails.name} >
{sliderDetails.bids && (<Sliders className="sliders" hiddenUserChange={hiddenUser} name={sliderDetails.name} userBids={sliderDetails.bids} setUserInformation={setUserInformation} userInformation={userInformation} />)}
</div>
)
})}
</div>
</div>
);
}
export default Broker;
ComboBox.jsx
import React, { useEffect, useRef, useState } from 'react';
import ComboBox from 'react-responsive-combo-box';
import { Button } from '#mui/material';
import 'react-responsive-combo-box/dist/index.css';
import "./style.css"
function Combo({ users, showUser, hideUser, userInformation }) {
const [selectedOption, setSelectedOption] = useState();
const [choosing, setChoosing] = useState();
useEffect(() => {
}, [users])
const onShow = () => {
showUser(users[selectedOption - 1].user)
}
const onHide = () => {
hideUser(users[selectedOption - 1].user)
}
const colorChange = (numOption) => {
const id = users[numOption - 1].user
}
return (
<div className="combo_box">
<ComboBox
onSelect={(option) => { setSelectedOption(option); colorChange(option) }}
options={[...Array.from({ length: users.length }, (_, i) => i + 1)]}
/>
<div className='button' >
<Button style={{ "marginRight": 20 }} variant="contained" onClick={onShow}>Show</Button>
<Button variant="contained" onClick={onHide}>Hide</Button>
</div>
</div>
);
}
export default Combo;
Sliders.jsx
import React, { useEffect, useState } from 'react'
import "./style.css"
import 'rc-slider/assets/index.css';
import Slider from 'rc-slider';
const Sliders = ({ hiddenUserChange, name, userBids, setUserInformation, userInformation }) => {
const [bids, setBids] = useState()
useEffect(() => {
setBids(Object.values(userBids))
}, [hiddenUserChange, userBids])
const updateFieldChanged = (newValue, e) => {//OnChanged Slider
setUserInformation(state => {
return state.map(manipulation => {
if (manipulation.name === name) {
Object.entries(manipulation.bids).forEach(([userId, bidValue], index) => {
manipulation.bids[userId] = newValue[index]
console.log(manipulation.bids[userId])
})
}
return manipulation
})
});
}
const handleChange = (event, newValue) => {
setBids(event)
};
return (
<>
<h1 className='headers'>{name}</h1>
{
<Slider
style={{ "marginRight": "20rem", "width": "30rem", "left": "20%" }}
range={true}
trackStyle={[{ backgroundColor: '#3f51b5' }]}
max={100}
RcSlider={true}
railStyle={{ backgroundColor: '#3f51b5' }}
activeDotStyle={{ left: 'unset' }}
ariaLabelForHandle={Object.keys(hiddenUserChange)}
tabIndex={(Object.keys(userBids))}
ariaLabelledByForHandle={bids}
value={(bids)}
onChange={handleChange}
onAfterChange={updateFieldChanged}
tipProps
tipFormatter
/>
}
</>
)
}
export default Sliders
enter image description here
Thank you all!
i had a game react matching card project to do.the game is consist to choose card in card grid if two carts contents is the same so this two cart will be her matched property true and will be displaying in green color and in the score component will mark 2 matched cards and so on if the card is not matched the cards color contents will be in red and another handler function will come here to make the two cards invisible by change the property of card object visible to false I had release this function it work well in the console but when I try it in the code application it not work at all please help me to fix it there is my code components :
the boardSlice:
const initialState = [
{id: 0, contents: 'Provider', visible: true, matched: true},
{id: 1, contents: 'Provider', visible: true, matched: true},
{id: 2, contents: 'selector', visible: true, matched: true},
{id: 3, contents: 'selector', visible: true, matched: true},
{id: 4, contents: 'useSelector()', visible: true, matched: true},
{id: 5, contents: 'useSelector()', visible: true, matched: true},
{id: 6, contents: 'useDispatch()', visible: true, matched: true},
{id: 7, contents: 'useDispatch()', visible: true, matched: true},
{id: 8, contents: 'Pure Function', visible: true, matched: true},
{id: 9, contents: 'Pure Function', visible: true, matched: true},
{id: 10, contents: 'react-redux', visible: true, matched: true},
{id: 11, contents: 'react-redux', visible: true, matched: true},
];
export const boardReducer = (state = initialState, action) => {
switch (action.type) {
case 'board/setBoard':
let setState = [];
action.payload.forEach((element, index) =>
setState.push({id: index,
contents: element,
visible: false,
matched: false})
);
return setState;
case 'board/flipCard':
let flipState = [...state];
const cardID = action.payload;
flipState[cardID] = {...state[cardID], visible:true}
const [index1, index2] = flipState.filter(card => card.visible).map(card => card.id);
if (index2 !== undefined) {
let card1 = flipState[index1];
let card2 = flipState[index2];
if (card1.contents === card2.contents) {
flipState[index1] = {...card1, visible: false, matched: true};
flipState[index2] = {...card2, visible: false, matched: true};
}
}
return flipState;
case 'board/resetUnmatchedCards':
let newState = [...state];
let [indexa, indexb] = newState.filter(card => card.visible === true && card.matched === false).map(card => card.id);
if (indexb !== undefined) {
let cardA = newState[indexa];
let cardB = newState[indexb];
newState[indexa] = {...cardA, visible: action.payload};
newState[indexb] = {...cardB, visible: action.payload}
}
return newState
case 'board/resetCards':
return state.map(card => ({...card, visible: false}));
default:
return state;
}
}
const wordPairs = [
'Provider', 'Provider',
'selector', 'selector',
'useSelector()', 'useSelector()',
'useDispatch()', 'useDispatch()',
'Pure Function', 'Pure Function',
'react-redux', 'react-redux',
]
const randomWords = () => {
let words = []
let newWordPairs = [...wordPairs]
const reps = newWordPairs.length
for (let i = 0 ; i < reps ; i++) {
const wordIndex = Math.floor(Math.random() * newWordPairs.length);
words.push(newWordPairs[wordIndex])
newWordPairs.splice(wordIndex, 1)
}
return words;
}
// action creators
export const setBoard = () => {
const words = randomWords()
return {
type: 'board/setBoard',
payload: words
}
}
export const flipCard = (id) => {
return {
type: 'board/flipCard',
payload: id
}
}
export const resetCards = (indices) => {
return {
type: 'board/resetCards'
}
};
export const resetUnmatchedCards = () => {
return {
type: 'board/resetUnmatchedCards',
payload: false
}
}
// Add selector export statments below
export const selectBoard = (state) => {
return ( state.board.map(card=>
({
id: card.id,
contents: card.contents
})
))}
export const selectVisibleIDs = state => {
return (
state.board.filter(card => card.visible)
.map(card => card.id)
)
}
export const selectMatchedIDs = state => {
return ( state.board.filter(card => card.matched)
.map(card => card.id));
};
the App component :
import './App.css';
import React from 'react';
import { Score } from './features/score/Score.js';
import { Board } from './features/board/Board.js';
import { useDispatch } from 'react-redux';
import { setBoard, resetCards } from './features/board/boardSlice';
// Add import statements below
const App = () => {
// Add dispatch variable below
const dispatch = useDispatch();
const startGameHandler = () => {
// Add action dispatch below
dispatch(setBoard())
};
const tryAgainHandler = () => {
// Add action dispatch below
dispatch(resetCards())
};
return (
<div className="App">
<Score />
<Board />
<footer className="footer">
<button onClick={startGameHandler} className="start-button">
Start Game
</button>
<button onClick={tryAgainHandler} className="try-new-pair-button">
Try New Pair
</button>
</footer>
</div>
);
};
export default App;
the board component :
import React from 'react';
import { CardRow } from './cardRow/CardRow.js';
// Add import statements below
import { useSelector } from 'react-redux';
import { selectBoard } from './boardSlice';
export const Board = () => {
// Add selected data variable and implement below
const currentBoard = useSelector(selectBoard);
const numberOfCards = currentBoard.length;
const columns = 3;
const rows = Math.floor(numberOfCards / columns);
const getRowCards = (row) => {
const rowCards = [];
for (let j = 0; j < columns; j++) {
const cardIndex = row * columns + j;
// Implement selected data below
rowCards.push(currentBoard[cardIndex]);
}
return rowCards;
};
console.log(currentBoard)
let content = [];
for (let row = 0; row < rows; row++) {
const rowCards = getRowCards(row);
content.push(
<CardRow
key={row}
cards={rowCards}
/>
);
}
return <div className="cards-container">{content}</div>;
};
the cardRow.js:
import React from 'react';
import { Card } from './card/Card.js';
import {selectMatchedIDs } from '../boardSlice'
export const CardRow = ({ cards }) => {
const content = cards.map(card =>
<Card
key={card.id}
id={card.id}
contents={card.contents}
/>)
return <>{content}</>;
};
the Card.js:
import React, {useEffect} from 'react';
// Add import statements below
import { useSelector, useDispatch } from 'react-redux';
import { selectVisibleIDs, flipCard, selectMatchedIDs, } from '../../boardSlice';
import { resetCards, resetUnmatchedCards } from '../../boardSlice'
let cardLogo = "https://static-assets.codecademy.com/Courses/Learn-Redux/matching-game/codecademy_logo.png";
export const Card = ({ id, contents }) => {
// Add selected data and dispatch variables below
const visibleIDs = useSelector(selectVisibleIDs)
const dispatch = useDispatch();
const matchedIDs = useSelector(selectMatchedIDs);
console.log(visibleIDs)
console.log(matchedIDs);
// flip card action
const flipHandler = (id) => {
// Add action dispatch below
dispatch(flipCard(id))
};
const resetHandler = () => {
dispatch(resetUnmatchedCards)
}
let cardStyle = 'resting';
let click = () => flipHandler(id);
let cardText = (
<img src={cardLogo} className="logo-placeholder" alt="Card option" />
);
// 1st if statement
// implement card id array membership check
if (visibleIDs.includes(id) || matchedIDs.includes(id)) {
cardText = contents;
click = () => {};
}
// 2nd if statement
// implement card id array membership check
if (matchedIDs.includes(id)) {
cardStyle = 'matched';
} else {
cardStyle = 'no-match';
}
// 3rd if statement
// implement number of flipped cards check
if (visibleIDs.length === 2) {
if (cardStyle === 'no-match' ) {
click = () => resetHandler();
}
click = ()=> {};
}
return (
<button onClick={click} className={`card ${cardStyle}`}>
{cardText}
</button>
);
};
so the problem is i want to change the click button to an other function to do so when first every card had this object {id:cardId, contents: cardContents, visible: false, matched: false} so first of when the user click two cards by the "FlipCard" action the card.visible become true then if the first card.contents is ht same as the second card.contents so the card.matched property for the two cards will become true so the require thing I want to add another function to reset the two card.visible to false when the first card.contents is not the same as the second card.contents so this is my code of the component Card :
import React, {useEffect} from 'react';
// Add import statements below
import { useSelector, useDispatch } from 'react-redux';
import { selectVisibleIDs, flipCard, selectMatchedIDs, } from '../../boardSlice';
import { resetCards, resetUnmatchedCards } from '../../boardSlice'
let cardLogo = "https://static-assets.codecademy.com/Courses/Learn-Redux/matching-game/codecademy_logo.png";
export const Card = ({ id, contents }) => {
// Add selected data and dispatch variables below
const visibleIDs = useSelector(selectVisibleIDs)
const dispatch = useDispatch();
const matchedIDs = useSelector(selectMatchedIDs);
console.log(matchedIDs);
// flip card action
const flipHandler = (id) => {
// Add action dispatch below
dispatch(flipCard(id))
};
const resetHandler = () => {
dispatch(resetCards)
}
let cardStyle = 'resting';
let click = () => flipHandler(id);
let cardText = (
<img src={cardLogo} className="logo-placeholder" alt="Card option" />
);
// 1st if statement
// implement card id array membership check
if (visibleIDs.includes(id) || matchedIDs.includes(id)) {
cardText = contents;
click = () => {};
}
// 2nd if statement
// implement card id array membership check
if (matchedIDs.includes(id)) {
cardStyle = 'matched';
} else {
cardStyle = 'no-match';
}
console.log(visibleIDs.every(id => matchedIDs.includes(id)))
// 3rd if statement
// implement number of flipped cards check
if (visibleIDs.length === 2 ) {
if (cardStyle === 'no-match' && visibleIDs.every(id => !matchedIDs.includes(id))) {
click = () => resetHandler();
} else {
click = () => {};
}
}
return (
<button onClick={click} className={`card ${cardStyle}`}>
{cardText}
</button>
);
};
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.
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.