I am working on carousel slider. In which i use 2 arrays. 1 array 'slideData' of objects which contain images and id's and the other array 'slideNum' contain indexs to iterate. slideArr is the final array which we will map, it contain images from 'slideData' and map according to 'slideNum' indexes. When i update 'slideArr' array with useState than is not updating but when i update directly using array.splice than its working.
const SecondGrid = () => {
const len = SlideData.length - 1;
const [first, setFirst] = useState(1);
const [second, setSecond] = useState(2);
const [third, setThird] = useState(3);
let [slideNum, setSlideNum] = useState([0, 1, 2]);
const [slideArr, setSlideArr] = useState([
{
id: 0,
imgsrc: "./assets/c1.jpg",
data: "Here is Light salty chineese touch",
},
{
id: 1,
imgsrc: "./assets/c2.jpg",
data: "Try our special breakfast",
},
{
id: 2,
imgsrc: "./assets/c3.jpg",
data: "Taste the italian likalu food",
},
]);
const next = () => {
setFirst(first >= len ? 0 : (prevState) => prevState + 1);
setSecond(second >= len ? 0 : (prevState) => prevState + 1);
setThird(third >= len ? 0 : (prevState) => prevState + 1);
// const arr = [...slideArr];
// storing next index image into all three Cards
slideNum.forEach((val, key1) => {
SlideData.forEach((value, key) => {
if (key === val) {
slideArr.splice(key1, 1, value);
// this is not working
// arr[key1] = value;
// console.log(arr);
// setSlideArr(arr);
//console.log(slideArr);
}
});
});
};
useEffect(() => {
next();
}, []);
useEffect(() => {
const interval = setTimeout(() => {
//updaing slideNum number,in which 'first' contain id of image which will be on 0 index, its updating through useState.
setSlideNum([first, second, third]);
next();
}, 3000);
return () => clearTimeout(interval);
}, [first, second, third]);
return (
<Container>
<div className="row">
<div className="d-flex flex-wrap justify-content-around ">
{slideArr.map((val) => (
<SlideCard val={val} />
))}
</div>
</div>
</Container>
);
};
It would be helpful to have some standalone code that demonstrates the problem but just looking at the commented out bit you say is not working, you can use map() to change values in an array:
setSlideArr(slideArr.map(item => (item.id === 1 ? value : item)))
Here's a demo CodeSandbox.
Also, your console.log(slideArr) should be in a useEffect hook if you want to see the state value after it has changed.
As far as I know, what happens is that you can't splice slideArr because it's useState, what you would be better off doing is:
if (key === val) {
var arr = slideArr;
arr.splice(key1, 1, value);
setSlideArr(arr);
}
Related
I'm trying to create a typing effect in React on 3 containers that are lined up and holding strings within them. The effect should start from the leftmost container after it finishes typing its string, it starts the container that comes after it, and so on.
I started with an idea where I store all the strings in an array and initialize a new array called currentText with a new letter every second but I probably just made things more complicated for myself.
Perhaps there is a simpler solution?
My complicated and not working solution looks like this:
const [text, setText] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [currentText, setCurrentText] = useState([]);
// Setting Text Content For Typing
useEffect(() => {
if(log && log.length > 0 && text.length == 0){
log.map((item, key) => {
let time = item['time'];
let message = item['message'];
let event = item['eventid'];
setText((prev) => [...prev, time, message, event]);
});
}
}, [log, text]);
useEffect(() => {
if(currentText?.length < text?.length){
const interval = setInterval(()=> {
// Setting Current index
if(currentText?.length !== 0 && currentIndex !== currentText?.length -1) {
setCurrentIndex(currentText?.length -1);
}
// Check if the last index string completed
if(currentText[currentIndex]?.length !== text[currentIndex]?.length){
let temp = currentText;
let lastText = temp[currentIndex];
if(lastText) lastText = lastText + text[currentIndex].charAt(lastText?.length -1);
else lastText = text[currentIndex].charAt(0);
temp[currentIndex] = lastText;
setCurrentText(temp);
}
// If completed open new array element to contain new string
else{
setCurrentText((prev) => [...prev, ['']]);
}
}, 1000);
return () => clearInterval(interval);
}
}, [currentText, text, currentIndex]);
return (
<>
{
currentText && currentText.length > 0 &&
currentText.map((item, key) => {
<div key={key} className={classes.row}>
<span className={classes.time}>{currentText[key]}</span>
<span className={classes.message}>{key % 1 ? currentText[key] : currentText[key+1]}</span>
<span className={classes.event}>{key % 2 ? currentText[key] : currentText[key+2]}</span>
</div>
})
}
The log look like this:
[
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
{time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string'},
]
// Define an array of log objects
const logs = [
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
{ time: '2023-02-19 06:25:30', message: 'some message', eventid: 'event_string' },
];
// Define a component that renders a paragraph tag with a substring of text
function TextComponent(props) {
const { index = 0, text } = props;
return <p> {text.substring(0, index)}</p>;
}
// Define a component that types out the text of its children components
function TypeWriter(props) {
// Destructure the props and get the number of children
const { children, charTime = 50 } = props;
const nChildrens = React.Children.count(children);
// Throw an error if there are no children
if (nChildrens < 1) throw new Error('Type writer component must have children');
// Define the state of the component, which keeps track of which child is currently active and how much of its text has been typed out
const [activeComponent, setActiveComponent] = React.useState({
children: 0,
index: 0,
length: children[0].props.text.length,
});
// Use the useEffect hook to update the state every time the interval elapses
React.useEffect(() => {
// Set an interval that updates the state by typing out a single character every time it is called
const interval = setInterval(() => {
setActiveComponent((state) => {
// If the current child's text has been completely typed out, move on to the next child
if (state.index > state.length) {
if (state.children < nChildrens - 1)
return {
index: 0,
children: state.children + 1,
length: children[state.children + 1].props.text.length,
};
// If there are no more children, clear the interval and return the current state
clearInterval(interval);
return state;
}
// Otherwise, update the state to type out the next character
return { ...state, index: state.index + 1 };
});
}, charTime);
// Return a function to clear the interval when the component unmounts
return () => {
clearInterval(interval);
};
});
// Render the children components, with the active child being typed out and the others already fully typed out
return (
<div>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
key: index,
index:
index === activeComponent.children
? activeComponent.index
: index < activeComponent.children
? child.props.text.length
: 0,
});
})}
</div>
);
}
// Define a component that renders a TypeWriter component with TextComponents as its children
export default function Home() {
return (
<TypeWriter>
{logs.map((log, index) => (
<TextComponent key={index} text={log.message} />
))}
</TypeWriter>
);
}
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 currently trying to use a Drag & Drop Library called dnd-kit and its hook called useSortable.
So far I did achieved to make everything draggable the way I like, unfortunately somehow its not possible for me to update the state accordingly to the dragging.
Attached you can find also an example of my code.
I'm glad for every suggestion to solve my problem :)
import React, { useState } from "react";
import "./styles.css";
import { DndContext } from "#dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "#dnd-kit/sortable";
import { CSS } from "#dnd-kit/utilities";
function SortableItem({ item }) {
const { id, name } = item;
const {
attributes,
listeners,
setNodeRef,
transform,
transition
} = useSortable({ id: id });
const style = {
transform: CSS.Transform.toString(transform),
transition
};
return (
<li ref={setNodeRef} style={style} {...attributes} {...listeners} draggable>
Movement for {name}
</li>
);
}
export default function App() {
const [items, setItems] = useState([
{ id: 1, name: "Items One" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Items 3" }
]);
const handleDragEnd = (event) => {
console.log(event);
const { active, over } = event;
if (active.id !== over.id) {
setItems((items) => {
console.log(items);
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
const newItemsArray = arrayMove(items, oldIndex, newIndex);
return newItemsArray;
});
}
};
console.log(items);
return (
<div className="App">
<h1>Sorting Example</h1>
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={items}>
{items.map((item) => (
<SortableItem key={item.id} item={item} />
))}
</SortableContext>
</DndContext>
</div>
);
}
You're main issue is in these lines:
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
Since you're using an array of objects and not just an array, you'll need to find the index through find as well and not just indexOf to get its index. Like so:
const oldItem = items.find({ id }) => id === active.id)
const newItem = items.find({ id }) => id === over.id)
const oldIndex = items.indexOf(oldItem);
const newIndex = items.indexOf(newItem);
const newItemsArray = arrayMove(items, oldIndex, newIndex);
If you're using typescript, any linting can complain that oldItem or newItem can be undefined and indexOf doesn't accept undefineds, so you can just use ! to force it because you know that the item will exist (usually), e.g.: const oldIndex = items.indexOf(oldItem!);
Update: or even better/cleaner using findIndex as suggested by #Ashiq Dey:
const oldIndex = items.findIndex({ id }) => id === active.id)
const newIndex = items.findIndex({ id }) => id === over.id)
const newItemsArray = arrayMove(items, oldIndex, newIndex);
The problem is with how you are trying to match id in indexOf method. if your items array looks like this
[
{id:1,name:"Item 1"},
{id:3,name:"Item 2"},
{id:4,name:"Item 3"}
]
then you should use the below code to make the match for respective id f.id === active.id but you were just passing the value of id and not making a conditional match.
function handleDragEnd(event) {
const { active, over } = event;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex(f => f.id === active.id);
const newIndex = items.findIndex(f => f.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
I'm trying to splice on a DoubleClick event an item from my list(inventory) but something is not working as I want it. Would be nice if someone could be so kind and help me out to figure out what I'm doing wrong here. For the Info: when I try to slice an item it gets sliced but it is every time the first item and the second item loses all the content inside:
function Inventory() {
const [datas, setData] = useState([
{
id: 1,
label: "Sword",
itemname: "grey_sword",
pieces: 1,
type: "weapon",
},
{
id: 2,
label: "Knife",
itemname: "grey_knife",
pieces: 1,
type: "weapon",
},
]);
useEffect(() => {
const getItems = (data) => {
setData(data);
} // this section is for something else
}, [datas]);
const deleteItem = (index) => {
const test = ([], datas)
test.splice(index, 1)
setData([{test : datas}]);
}
const renderItem= (data, index) => {
return (
<Item
key={data.id}
id={data.id}
type={data.type}
label={data.label}
index={index}
name={data.itemname}
pieces={data.pieces}
deleteItem={deleteItem}
/>
)
}
return (
<div className="inventory-holder">
<div className="inventory-main">
<div className="inventory-information">
<div className="inventory-title">
Inventory
</div>
<div className="inventory-weight-info">
0.20 kg / 1.00 kg
</div>
<div className="inventory-buttons">
<button className="refresh-button" tabIndex="-1"><FontAwesomeIcon icon={faRedo} /></button>
<button className="close-button" tabIndex="-1"><FontAwesomeIcon icon={faTimes} /></button>
</div>
</div>
<div className="items-holder">
<div>{datas.map((data, i) => renderItem(data, i))}</div>
</div>
</div>
</div>
)
}
export default Inventory;
and that would be the Item:
const Item = ({ index, id, type, label, name, pieces, deleteItem }) => {
const useItem = e => {
const type = e.target.getAttribute("itemType");
const index = e.target.getAttribute("data-index");
const pieces = e.target.getAttribute("itempieces");
console.log(type + " " + index + " " + pieces )
if(parseInt(pieces) <= 1){
deleteItem(parseInt(index));
}
}
return(
<div data-index={id} onDoubleClick={useItem} className="inventory-item" itemType={type} itemname={name} itemlabel={label} itempieces={pieces}>
<img className="item-pic" src={chosedtype} ></img>
<div className="item-label">{label}</div>
<div className="item-number-pieces">{pieces}</div>
</div>
);
};
export default Item;
Issue
Array::splice does an in-place mutation.
The splice() method changes the contents of an array by removing or
replacing existing elements and/or adding new elements in place.
The main issue here is state mutation.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator
const test = ([], datas) ends up saving the state reference data to test, which is then mutated by test.splice(index, 1), and then strangely enough is overwritten back into state differently setData([{ test: datas }]);.
Solution
A common pattern is to use array::filter instead and filter on the index. filter returns a new array.
const deleteItem = (index) => {
setData(datas => datas.filter((_, i) => i !== index);
}
Your Item is way too complicated:
You can directly use the props without passing them through your div.
const Item = ({ index, id, type, label, name, pieces, deleteItem }) => {
const useItem = () => {
console.log(type + " " + index + " " + pieces )
if(pieces <= 1){
deleteItem(index);
}
}
return(
<div data-index={id} onDoubleClick={useItem} className="inventory-item">
<img className="item-pic" src={chosedtype} ></img>
<div className="item-label">{label}</div>
<div className="item-number-pieces">{pieces}</div>
</div>
);
};
export default Item;
Then your deleteItem function doesn't do what you want:
const deleteItem = (index) => {
const test = ([], datas); //test = datas;
test.splice(index, 1); // test = test without the item at the index
setData([{test : datas}]);//data = [{test: datas}] so an array with an object with the property test = datas (the original array).
}
You should change your deleteItem to something like:
const deleteItem = (index) => {
const newArray = datas.filter((item, i) => i !== index);
setData(newArray);
}
I've got the following search suggest with React hooks that uses react-hotkeys-hooks to manage keypress.
Why does selectedUserItem not update on keypress Enter? It stays 0 while the up and down keys change.
import { useHotkeys } from "react-hotkeys-hook";
import React, { useState } from "react";
import "./styles.css";
const itemsByName = [
{
id: 1,
name: "Ice Cream"
},
{
id: 2,
name: "Banana Pudding"
},
{
id: 3,
name: "Chocolate Cake"
},
{
id: 4,
name: "Sponge Cake"
},
{
id: 5,
name: "Carrot Cake"
}
];
const App = () => {
const [selectedUserItem, setSelectedUserItem] = useState(0);
// const [create] = useMutation(SAVE_USER_ITEM, {
// refetchQueries: ["UserItemsQuery"]
// })
const itemSelect = (e, item) => {
e.preventDefault();
// create({ variables: { input: { id: item.id } } });
// console.log(item)
};
const increment = selectedUserItem => {
const max = itemsByName.length - 1;
return max > selectedUserItem ? selectedUserItem + 1 : max;
};
const decrement = selectedUserItem => {
const min = 0;
return min < selectedUserItem ? selectedUserItem - 1 : min;
};
useHotkeys(
"*",
(event, handler) => {
// console.log(handler)
switch (event.key) {
case "ArrowDown":
setSelectedUserItem(selectedUserItem => increment(selectedUserItem));
break;
case "ArrowUp":
setSelectedUserItem(selectedUserItem => decrement(selectedUserItem));
break;
case "Enter":
console.log(selectedUserItem);
const userItem = itemsByName[selectedUserItem];
console.log(userItem);
break;
default:
console.log(event.key);
break;
}
},
{
filter: () => true
}
);
return (
<div className="absolute w-3/4 mt-16 ml-8 py-2 bg-white shadow-xl rounded-lg">
<h1>Index: {selectedUserItem}</h1>
{itemsByName.map((item, i) => {
return (
<div
href="#"
onClick={e => itemSelect(e, item)}
className={`${selectedUserItem === i ? "hovered" : ""} dessert`}
key={item.id}
>
{item.id}: {item.name}
</div>
);
})}
</div>
);
};
export default App;
useHotkeys internals use the useCallback and useEffect hooks, which need to know when some of its dependencies change. To make sure it works well with these hooks, useHotkeys offers to pass a deps array, like the other hooks mentioned, as its last parameter.
deps: any[] = []: The dependency array that gets appended to the memoization of the callback. Here you define the inner dependencies of your callback. If for example your callback actions depend on a referentially unstable value or a value that will change over time, you should add this value to your deps array. Since most of the time your callback won't depend on any unstable callbacks or changing values over time you can leave this value alone since it will be set to an empty array by default.
In your code, it would looks like this:
// These never changes and do not rely on the component scope, so they
// can be defined safely outside the component.
const increment = selectedUserItem => {
const max = itemsByName.length - 1;
return max > selectedUserItem ? selectedUserItem + 1 : max;
};
const decrement = selectedUserItem => {
const min = 0;
return min < selectedUserItem ? selectedUserItem - 1 : min;
};
const App = () => {
const [selectedUserItem, setSelectedUserItem] = useState(0);
useHotkeys(
"*",
(event, handler) => {
switch (event.key) {
case "ArrowDown":
setSelectedUserItem(increment);
break;
case "ArrowUp":
setSelectedUserItem(decrement);
break;
case "Enter":
console.log(selectedUserItem, itemsByName[selectedUserItem]);
break;
default:
console.log(event.key);
break;
}
},
{
filter: () => true
},
// The dependencies array which ensure that the data is up to date in the callback.
[selectedUserItem, setSelectedUserItem]
);
// rest of the component