I have a code which I pass it to the onClick event and I want it to be run 3 times: (this code basically choose a random product from products and then by Hooks I update the state to show it in the shop cart and here actually instead of one I'm planing to show 3 random products. so I was thinking is there way to simply fire onClick 3 times with one click. )
const onRandom = () => {
const randomItem = products[Math.floor(Math.random() * products.length)];
const exist = cartItems.find((x) => x.id === randomItem.id);
if (exist) {
setCartItems(
cartItems.map((x) =>
x.id === randomItem.id ? { ...exist, qty: exist.qty 1 } : x
)
);
} else {
setCartItems([...cartItems, { ...randomItem, qty: 1 }]);
}
};
I have used numourous ways but none of them seems working ...
const onRandom = () => {
const randomItem = products[Math.floor(Math.random() * products.length)];
const exist = cartItems.find((x) => x.id === randomItem.id);
if (exist) {
setCartItems(
cartItems.map((x) =>
x.id === randomItem.id ? { ...exist, qty: exist.qty 1 } : x
)
);
} else {
setCartItems([...cartItems, { ...randomItem, qty: 1 }]);
}
onRandom2();
};
const onRandom2 = () => {
const randomItem2 = products[Math.floor(Math.random() * products.length)];
const exist2 = cartItems.find((x) => x.id === randomItem2.id);
if (exist2) {
setCartItems(
cartItems.map((x) =>
x.id === randomItem2.id ? { ...exist2, qty: exist2.qty 1 } : x
)
);
} else {
setCartItems([...cartItems, { ...randomItem2, qty: 1 }]);
}
};
I have also used for and while and changed the const to var and still I'm getting one random product and there is even no error.
maybe I'm missing something about state implication.
CodePudding user response:
Let me demonstrate the issue using a simple example:
function App() {
const [counter, setCounter] = React.useState(0);
// Only adds 1 per click because counter stays the same (you
// are working with an outdated counter). So you are simply
// setting it 3 times to an increment of 1 using the same outdated
// value for all 3 increments.
function add3() {
setCounter(counter 1); // setCounter(0 1);
setCounter(counter 1); // setCounter(0 1);
setCounter(counter 1); // setCounter(0 1);
}
// Does add 3 per click because the counter passed to
// the callback is the updated version.
function add3Functional() {
setCounter(counter => counter 1); // setCounter(0 1);
setCounter(counter => counter 1); // setCounter(1 2);
setCounter(counter => counter 1); // setCounter(2 3);
}
return (
<div>
{counter}
<button onClick={add3}>add 3 (non-functional)</button>
<button onClick={add3Functional}>add 3 (functional)</button>
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
Here is the relevant React documentation:
Functional updates
If the new state is computed using the previous state, you can pass a function to
setState
. The function will receive the previous value, and return an updated value. Here’s an example of a counter component that uses both forms ofsetState
:function Counter({initialCount}) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount 1)}> </button> </> ); }
The ” ” and ”-” buttons use the functional form, because the updated value is based on the previous value. But the “Reset” button uses the normal form, because it always sets the count back to the initial value.
In short, if you want to update a state multiple times before a new render you should always use a functional update. In your scenario this means changing:
setCartItems( cartItems.map((x) => x.id === randomItem.id ? { ...exist, qty: exist.qty 1 } : x ) );
Into
setCartItems((cartItems) => ( // <- uses the updated cartItems value
cartItems.map((x) => (
x.id === randomItem.id ? { ...exist, qty: exist.qty 1 } : x
))
));
The same applies for your else scenario.
After changing your onRandom
function to use functional updates you can wrap the contents in a for-loop.
const onRandom = () => {
for (let i = 0; i < 3; i) {
// add random item using functional update
}
};
Or alternatively create a function that holds the logic and call it 3 times.
function onRandom() {
addRandom();
addRandom();
addRandom();
}