Why does a useState value isn't updated inside useHotkeys callback? - javascript

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

Related

React typing effect on 3 containers with strings

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

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.

useState not updating array correctly

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

How to dynamically add isFixed if the items length is one in react-select?

I want to make a multi filter so that user can remove any of the item until items length is one. I want to add isFixed = true in the object dynamically when user remove 2 out of 3 items for this below example.
import React, { useState } from 'react';
import Select from 'react-select';
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'orange', label: 'Orange' },
{ value: 'grape', label: 'Grape' },
];
const orderOptions = (values) =>
values && values.filter((v) => v.isFixed).concat(values.filter((v) => !v.isFixed));
export default function TestSelect() {
const [value, setValue] = useState(orderOptions([...options]));
console.log('value', value);
const onChange = (value, { action, removedValue }) => {
switch (action) {
case 'remove-value':
case 'pop-value':
if (removedValue.isFixed) {
return;
}
break;
case 'clear':
value = options.filter((v) => v.isFixed);
break;
default:
break;
}
value = orderOptions(value);
setValue(value);
};
return (
<div>
<Select
value={value}
isMulti
isClearable={value && value.some((v) => !v.isFixed)}
name="fruit"
classNamePrefix="select"
onChange={onChange}
options={options}
defaultValue={options}
/>
</div>
);
}
You just have to check if there's only one element remaining in the values array within the switch-case. To do so, just add two lines:
...
const onChange = (value, { action, removedValue }) => {
switch (action) {
case "remove-value":
case "pop-value":
if (removedValue.isFixed) return;
if (value.length === 1) value[0].isFixed = true; // <-- add the isFixed element
break;
case "clear":
value = options.filter((v) => v.isFixed);
break;
default:
value.forEach(x => x.isFixed = false) // <-- remove the isFixed element
break;
}
value = orderOptions(value);
setValue(value);
};
...
Remember to add the styles part to make it more visual. As you can see here.

React Hooks and localStorage dont set correct value

Hello I had an idea to make a hook to increase the font size and save preferences in localStorage
basically I have a state that goes from 1 to 4, and then when I click the button add I add +1 to the state until I reach number 4
and on the remove button I remove 1 from the state until 1
But I have doubts on how to save this to my location
basically if i don't use my useState just with getInitialValue It works normally.
like this gif, If I add the value manually it works:
but if I try to use my setFont I have problems (as it is saved in localStorage):
and i got this on localStorage :
code:
export default function App() {
const { fontSize, setSize } = useFontSize();
console.log(fontSize);
return (
<div className="App">
<button
onClick={() => {
setSize(fontSize + 1);
}}
>
add
</button>
<button
onClick={() => {
setSize(fontSize + 1);
}}
>
remove
</button>
</div>
);
}
hook:
export default function useFontSize(defaultSize = { size: 1 }) {
const [fontSize, _setSize] = useState(getInitialSize);
function getInitialSize() {
const savedSize = localStorage.getItem('_size_acessibility_font');
const parsedSize = JSON.parse(savedSize);
if (parsedSize) {
const { size } = parsedSize;
if (size >= 1 && size <= 4) {
return size;
}
} else {
return defaultSize.size;
}
}
useEffect(() => {
console.log(fontSize, 'on useEffect to set on localStorage');
localStorage.setItem(
'_size_acessibility_font',
JSON.stringify({ size: fontSize }),
);
}, [fontSize]);
return {
fontSize,
setSize: ({ setSize, ...size }) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
}
example:
https://codesandbox.io/s/focused-newton-x0mqd
I don't know if this is the best logic for this context, if someone can help me.
This seems a tad overengineered and upsets a few hooks idioms. For example, returning a named object pair for a hook is less typical than an array pair. The set function itself is complex and returns the result of the _setSize calls. Naming could be clearer if fontSize matched setSize by using setFontSize.
({ setSize, ...size }) is problematic since the caller is (correctly) providing an integer.
Here's a minimal, complete version that fixes these issues (local storage is mocked since Stack Snippets is sandboxed):
const localStorageMock = (() => {
const storage = {};
return {
getItem: k => storage[k],
setItem: (k, v) => {storage[k] = v.toString();}
};
})();
const {useState, useEffect} = React;
const useFontSize = (defaultSize=1) => {
const clamp = (n, lo=1, hi=4) => Math.min(hi, Math.max(n, lo));
const clean = n => isNaN(n) ? defaultSize : clamp(+n);
const storageName = "_size_acessibility_font";
const fromStorage = clean(localStorageMock.getItem(storageName));
const [fontSize, setFontSize] = useState(fromStorage);
useEffect(() => {
localStorageMock.setItem(storageName, fontSize);
}, [fontSize]);
return [fontSize, size => setFontSize(clean(size))];
};
const App = () => {
const [fontSize, setFontSize] = useFontSize();
return (
<div>
<div>Font size: {fontSize}</div>
<button onClick={() => setFontSize(fontSize + 1)}>
+
</button>
<button onClick={() => setFontSize(fontSize - 1)}>
-
</button>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
In useFontSize, you return
return {
fontSize,
setSize: ({ setSize, ...size }) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
However, in App, you call setSize with just a number setSize(fontSize + 1); when it is expecting an object.
If you change useFontSize to return
return {
fontSize,
setSize: (size) => {
console.log(size, 'on function set size');
if (size > 4) {
return _setSize(4);
}
if (size < 1) {
return _setSize(1);
}
return _setSize(size);
},
};
It should work.
Note, you will want to clear your current local storage, or add some error checking.
Also note, although it is just an example, both add and remove use fontSize + 1

Categories