Removing items from state by timer - javascript

There is a local state (hook), it has an array of four elements. There is a button on the screen to add a new element to this array. When a component is loaded, in useEffect called method that removes the first element from the state every 5 seconds. If you do not touch the button that adds a new element to the state, then everything works great. But if you start adding elements, the deletion works according to the previous state, only then the state with the new element is displayed. Tell me how to fix it so that everything works stably. I understand what needs to be sought in the direction of the life cycle, a conflict of states occurs, but I can not find a solution.
const Component = () => {
const [arr, setArr] = useState(['one', 'two', 'three', 'four']);
React.useEffect(() => {
console.log("render");
setTimeout(deleteElementFromArr, 5000)
});
const addNewElementToArr = () => {
let temp = arr.slice();
temp.push('newElement');
setArr(temp);
};
const deleteElementFromArr = () => {
if (arr.length > 0) {
console.log(arr);
let temp = arr.slice();
temp.splice(0, 1);
setArr(temp)
}
};
return (<div>
<div>
<Button onClick={addNewElementToArr}>add</Button>
</div>
<div style={{margiTop: '10px'}}>
{arr.map(a => `${a} `)}
</div>
</div>)
};
https://codepen.io/slava4ka/pen/WNNvrPV

In your useEffect hook, when the effect is finished, clear the timeout. When the state is changed, it will trigger again with the new value of the state.
React.useEffect(() => {
console.log("render");
const timer = setTimeout(deleteElementFromArr, 5000);
return () => clearTimeout(timer);
});

Related

draggable columns in ReactJs Antd table causes multi renders of useEffect

I am using antd for create a table in reactJs.
I added eventListener to th tag because I want to be able to drag the columns.
I do it with useRef to get all the th tags:
const [cols, setCols] = useState(columns); // -> columns is static array of object
const tableRef = useRef(null);
useEffect(() => {
if (tableRef.current) {
let elems = tableRef.current.ownerDocument.querySelectorAll('th');
for (let i = 0; i < elems.length; i++) {
elems[i].setAttribute('draggable', true);
elems[i].addEventListener('dragstart', (e) => {
handleDragStart(e);
});
elems[i].addEventListener('dragover', (e) => {
handleDragOver(e);
});
elems[i].addEventListener('drop', (e) => {
handleOnDrop(e);
});
}
}
}, [cols]); // -> cols is the updated columns after change order
const handleDragStart = (e) => {
const { innerText } = e.target;
const idx = cols.findIndex((x) => x.title === innerText);
e.dataTransfer.setData('colIdx', parseInt(idx));
};
const handleDragOver = (e) => {
e.preventDefault();
};
const handleOnDrop = (e) => {
const { innerText } = e.target;
const droppedColIdx = cols.findIndex((x) => x.title === innerText);
const draggedColIdx = parseInt(e.dataTransfer.getData('colIdx'));
setCols(() => {
const tempCols = [...cols];
tempCols.splice(droppedColIdx, 0, tempCols.splice(draggedColIdx, 1)[0]);
return tempCols;
});
};
My problem:
The columns are draggable and I can move them.
If in the useEffect dependency I add cols array, in the first drag the useEffect will call one time, in the second drag the useEffect will call 2 times, in the third drag the useEffect will call 4 times, in the fourth drag the useEffect will call 8 times and so on.
That is every time the useEffect is call, it multiple the call in 2.
If I remove cols array from useEffect dependency, after the first drag everything is ok, but in the second drag my cols reset and initialized to columns.
Thank you for helping me.
You need to remove the listeners in the return of the useEffect in the following manner:
useEffect(() => {
const handleClick = event => {
console.log('Button clicked');
};
const element = ref.current;
element.addEventListener('click', handleClick);
// 👇️ remove the event listener when component unmounts
return () => {
element.removeEventListener('click', handleClick);
};
}, []);

How to count for each mapped element?

Solved - wasn't aware of the useRef hook which helped me track each individual mapped item.
I have a set of results mapped out within a card element. I want to keep a click count for each of those elements, but with a global JS variable, it counts the clicks of all elements if I call that variable on more than one clickable element per session. I have tried to do id.index, adding (id) + index etc but am stumped. How do I properly use the unique id's to track the index for each card? Thanks
function onClick(id) {
let index = 0;
index++;
if (index >= 1) {
dosomething
} else if (index === 0) {
dosomethingelse
}
}
It's not clear what and how you want to count and onclick events.
Assuming that you need to keep track of clicks on each element/id:
You can use the useRef hook and keep it a global object to track the number of clicks per id.
const clicksPerId = useRef({});
function onClick(id) {
if (!clicksPerId.current[id]) {
clicksPerId.current[id] = 0;
}
clicksPerId.current[id]++;
// whatever you want to do with the clicks count
}
I'm kinda confused by your question to be honest however for working with arrays in javascript / React maybe you'll find some of these helpful
Getting the array length
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
// find the length of the array
const getArrayLength = () => {
return myArray.length;
}
return (
<p>hello there</p>
)
}
Doing something with the index of a maped component:
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
const handleClick = (index) => {
// do somthing with the index of the el
};
return (
<>
{ myArray.map((el, index) => {
return (
<p
key={index}
onClick={() => handleClick(index)}
>
el number { el }
</p>
)
})
}
</>
)
}

For loops and if statements in Hooks?

Is there a way where I can use for loops and if statements without breaking the hook rule? To elaborate, I am currently trying to compare two lists (allData and currentSelection) and if there are similarities, I will add them to another list (favData). However, I am constantly either having visibility issues or errors. If I can get some help, I would much appreciate it!
const [favData, setFavData] = useState([]);
useEffect(() => {
getFilterFavMeal();
}, []);
function getFilterFavMeal() {
allData.forEach((mealList) => {
currentSelection.forEach((mealList2) => {
if (mealList["menu_item"]["menu_item_id"] === mealList2.value) {
// with push, I have visibility issues
// favData.push(mealList);
setFavData(mealList);
}
});
});
setFavData(favData);
}
The set function that useState returns updates the state and schedules a re-render of the component so that the UI can update. It doesn't make sense to call the set function many times in one render.
You also don't want to mutate React state by using functions like push.
Since it looks like favData is deterministic, you can simply remove it from the component state and calculate it in the render loop.
const favData = allData.filter(a => currentSelection.some(c => c.value === a.menu_item.menu_item_id));
Answering your original question, of course you can use loops. As long as you don't mutate the existing state object. And don't set the state more than once per render.
const FF = () => {
const [list, setList] = useState([]);
const addStuffToList = () => {
const tail = Array.from(new Array(3)).map((_e, i) => i);
// Build a new array object and use that when setting state
setList([...list, ...tail]);
}
const forLoop = () => {
const tail = [];
for (let i = 0; i < 4; i++) {
tail.push(i);
}
// Same thing
setList([...list, ...tail]);
}
return ...
};

React: setState doesn't re-render the component

I'm implementing a shopping cart for a ecommerce website. The shopping cart is a state variable shopCart represented by an array of objects. Each object contains information about a product, such as title and price. I am trying to implement a remove button, which is actually doing what is intended from it, which is to remove items from the shopCart state, but the changes are not represented on the screen render. I can empty the cart, but the screen still shows the products. Here is the main code of the shopping cart page:
return (
<div class={styles.container}>
<h1>Product</h1><h1>Quantity</h1><h1>Unit price</h1><h1>Total price</h1><div></div>
{
shopCart.map((product, i, array) => <CartItem key={product.id} product={product} index={i} array={array}/>)
}
</div>
)
And here is the implementation of CartItem.js
const CartItem = (props) => {
let { shopCart, setShopCart } = useContext(Context);
let product = props.product;
// takes the identification of a shopping cart product and removes it from the cart
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart;
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={() => {
decrease(product.id);
}}>Remove</button>
</div>
)
}
Why isn't it rendering the cart correctly, even though the cart items are being removed after each click of the remove button ?
Issue
You are mutating state. You save a reference to state, mutate it, then save it back into state, so the array reference never changes. React uses shallow equality when checking if state or props update.
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart; // <-- Saved state ref
aux.splice(i, 1); // <-- mutation
setShopCart(aux); // <-- Saved ref back to state
}
}
})
}
Solution
The correct way to update arrays in react state is to copy the array elements into a new array reference. This can be easily accomplished by filtering the current cart by item id. I also suggest changing the argument name so it is clearer what it represents.
const decrease = (id) => {
setShopCart(shopCart => shopCart.filter(item => item.id !== id));
}
You're modifying the shopCart (aux is a reference) directly which is both the context and the collection that you're iterating over. You need to make sure you're updating a copy of the shopping cart and resetting the context. Minimally, you can do the following:
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart.slice(); // makes a copy
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
However, I suggest using the approach Drew recommended. The current approach isn't ideal.
The solution is much simpler than you think. You can use array.filter to remove the matching product by id.
const CartItem = (props) => {
const { product} = props;
let { shopCart, setShopCart } = useContext(Context);
// takes the identification of a shopping cart product and removes it from the cart
const handleClick = (e) => {
const filteredShopCart = shopCart.filter(item => item.id !== product.id);
setShopCart(filteredShopCart);
};
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={handleClick}>Remove</button>
</div>
);
};

react save possibility to select text with re-render component

i have component with text. It changed component own state with mouse click. But i want to save possibility to select and copy in by long click. Is there way to make it ? Selection is reset after rerender component. Code for example:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const clickHandler = () => {
setSomeState(someState + 1);
}
return (
<div
className="App"
onClick={clickHandler}
>
{'State ' + someState}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
How about using onMouseDown and onMouseUp events yourself and calculate the time the user took to click instead of using onClick?
You could for example do something like this:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const [timeDown, setTimeDown] = React.useState(-1);
const clickHandler = () => setSomeState(someState + 1);
const handleMouseDown = () => setTimeDown(Date.now()); // Save the time of the mousedown event
const handleMouseUp = () => {
const timeUp = Date.now();
const timeDiff = timeUp - timeDown; // Calculate the time the user took to click and hold
if (timeDiff < 1000) { // If it's shorter than 1000ms (1s) execute the normal click handler
clickHandler();
} else { // Execute some other logic, or just ignore the click
// handleLongClick();
}
};
return (
<div
className="App"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
{"State " + someState}
</div>
);
};
You can find a quick codesandbox as a demo here

Categories