shuffle function called every time state is updated? - javascript

The problem simply is, i have a shuffle function that shuffles array of numbers,
the numbers rendered as cards in a deck, the app is simple, it needs when clicking two cards with same number, they get the same color.
so i created a state which is an array which receives only two cards to compare, once comparing is complete, the array length returns to 0 then push two cards again and so on.
now the problem is shuffle function works again and again every time state is updated and this makes cards re-render every time with different numbers(shuffled)
code:
const icons = [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 5, 6, 7, 8];
const shuffle = (cards) => {
let counter = cards.length;
// While there are elements in the array
while (counter > 0) {
// Pick a random index
let index = Math.floor(Math.random() * counter);
// Decrease counter by 1
counter--;
// And swap the last element with it
let temp = cards[counter];
cards[counter] = cards[index];
cards[index] = temp;
}
return cards;
}
const shuffledCards = shuffle(icons);
const [cards, setCards] = useState([]);
const [isCorrect, checkCorrect] = useState(false)
const addCard = (card) => {
if (cards.length < 2) {
setCards([...cards, card]);
}
if(cards.length === 2) {
compareCards(cards);
setCards([]);
}
}
const compareCards = (cards) => {
if(cards[0] === cards[1] ) {
checkCorrect(true);
}
}
return (
<div className="App">
<Game shuffledCards={shuffledCards} addCard={addCard} />
</div>
);
}
const Game = (props) => {
const { shuffledCards, addCard } = props;
return (
<div className="game">
<div className="deck">
{
shuffledCards.map((c, i) => {
return (
<div className="card" key={i} onClick={() => addCard(c)}>{c}</div>
);
})
}
</div>
</div>
)
}
export default App;

you can use useEffect:
const [cards, setCards] = useState([]);
useEffect(()=>{shuffle()},[cards])

Related

Why do I get NaN value in react?

Whilst I am doing cart in react, I have no idea why I keep getting NaN value - only from a specific object data.
When I have the following data list:
#1 ItemsList.js
export const ItemsList = [
{
id: 1,
name: "VA-11 Hall-A: Cyberpunk Bartender Action",
price: 110000,
image: cover1,
link: "https://store.steampowered.com/app/447530/VA11_HallA_Cyberpunk_Bartender_Action/?l=koreana",
},
...
{
id: 6,
name: "Limbus Company",
price: 110000,
image: cover6,
link: "https://limbuscompany.com/",
},
];
And the following code, please look at the comment line.
#2 Goods.jsx
import React, { useContext } from "react";
import "./Goods.css";
import { DataContext } from "../../components/context/DataContext";
export const Goods = (props) => {
const { id, name, price, image, link } = props.shopItemProps;
const { cartItems, addItemToCart, removeItemFromCart } =
useContext(DataContext);
const cartItemStored = cartItems[id];
return (
<div className="goods">
<div className="goods-id">{id}</div>
<img src={image} alt="thumbnail_image" className="goods-image" />
<div className="goods-name">{name}</div>
<div className="goods-price">${price}</div>
<button>
<a href={link} className="goods-link">
Official Store Page
</a>
</button>
<div className="cart-button">
<button onClick={() => removeItemFromCart(id)}>-</button>
// ★Maybe here? but why do I get NaN only for id:6? Others work well.
{cartItemStored > -1 && <> ({cartItemStored}) </>}
<button onClick={() => addItemToCart(id)}>+</button>
</div>
</div>
);
};
What should I do to solve NaN? There seems to be no way to make that value as int in this case. Or do you see any problem from the above code block?
Edited
Sorry for confusing you. Here are the additional code related.
#3. DataContext.js (where cartItems state exists)
import React, { createContext, useState } from "react";
import { ItemsList } from "../ItemsList";
export const DataContext = createContext(null);
const getDefaultCart = () => {
let cart = {};
for (let i = 1; i < ItemsList.length; i++) {
cart[i] = 0;
}
return cart;
};
export const DataContextProvider = (props) => {
const [cartItems, setCartItems] = useState(getDefaultCart);
const checkoutTotalSum = () => {
let totalAmount = 0;
for (const item in cartItems) {
if (cartItems[item] > 0) {
let itemInfo = ItemsList.find((product) => product.id === Number(item));
totalAmount += cartItems[item] * itemInfo.price;
}
}
return totalAmount;
};
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
const removeItemFromCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] - 1 }));
};
const updateCartItemCount = (newAmount, itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: newAmount }));
};
const contextValue = {
cartItems,
addItemToCart,
removeItemFromCart,
updateCartItemCount,
checkoutTotalSum,
};
// console.log(cartItems);
return (
<DataContext.Provider value={contextValue}>
{props.children}
</DataContext.Provider>
);
};
The issue is in the function you are using to set the initial value of cartItems, more specifically, in the for loop. This line is the culprit: i < ItemsList.length, when in your case, it should be i <= ItemsList.length. Why? because you are not including the last element of ItemsList on the cart object (you are initializing the i counter with 1 and ItemsList's length is 6).
So, when you call addItemToCart:
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
And try to update the value corresponding to the last element of ItemsList which is 6 in cartItems, you're getting: '6': undefined + 1 because again, you did skip the last element in the for loop. This results in NaN.
You also have the option of initializing i with 0 and preserve this line: i < ItemsList.length, or:
for (let i = 1; i < ItemsList.length + 1; i++) {
...
}

First value in array is not getting updated

I'm building a shopping cart where the user can change the amount they want to invest per asset, and then see how much their investment is worth when compounded 10% over 5 years.
I first have my Checkout component:
import React, { useEffect, useState } from "react";
import CartInner from "./CartInner";
export default function Checkout() {
const [cart, setCart] = useState<any>([
{ price: 2000, id: 1 },
{ price: 4000, id: 2 }
]);
const [projectedGrowth, setProjectedGrowth] = useState<any>([]);
const handleOnChange = (index: any, e: any) => {
const newValue = {
price: Number(e.target.value),
id: index + 1,
property_id: index + 1
};
const newArray = [...cart];
newArray.splice(index, 1, newValue);
setCart(newArray);
};
const getGrowthAmount = (index: any) => {
console.log(cart[index].price, "default values for price");
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
const newArray = [...projectedGrowth];
newArray.splice(index, 1, newValue);
setProjectedGrowth(newArray);
};
// whenever the cart is updated call the getGrowthAmount function
useEffect(() => {
for (let index = 0; index < cart.length; index++) {
getGrowthAmount(index);
}
}, [cart]);
console.log(projectedGrowth, "projectedGrowth");
console.log(cart, "cart");
return (
<>
<form className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{cart.map((element, index) => {
return (
<fieldset key={index}>
<CartInner
// projectedGrowth={projectedGrowth[index]}
element={element}
handleOnChange={(e) => handleOnChange(index, e)}
defaultValues={cart[index]?.price}
/>
</fieldset>
);
})}
</div>
<button>Proceed to payment</button>
</form>
</>
);
}
Which allows the user to change their investment amount and calculate compound interest:
function calcTotalCompoundInterest(total: any, year: any, rate: any) {
var interest = rate / 100 + 1;
return parseFloat((total * Math.pow(interest, year)).toFixed(4));
}
My problem is only the second value is getting updated, not the first. E.g if the first input I write 100 and the second 100,000 then the projectedGrowth array will return:
(2) [6442.04, 16105.1]
Which is correct for the second amount (but not for the first).
Here's the Codesandbox
Here's the child component:
import PureInput from "./PureInput";
import React, { useEffect, useState, useMemo } from "react";
const CartInner = React.forwardRef(
(
{
handleOnChange,
price,
projectedGrowth,
defaultValues,
id,
...inputProps
}: any,
ref: any
) => {
return (
<input
min={200}
max={price}
handleOnChange={handleOnChange}
type="number"
step={200}
defaultValue={defaultValues}
id={id}
ref={ref}
{...inputProps}
/>
);
}
);
export default CartInner;
What am I doing wrong here?
How can I get the array to return the correct compounded values (both Onload and Onchange when the user enters into the input)?
Issue is with the splice. accessing the index directly from the array works
const getGrowthAmount = (index: any) => {
console.log(cart[index].price, "default values for price");
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
console.log(newValue, "newValue");
projectedGrowth[index] = newValue;
console.log(projectedGrowth, "list");
setProjectedGrowth(projectedGrowth);
};
Demo
The correct code is this one:
import React, { useEffect, useState, useMemo } from "react";
import { useForm, Controller } from "react-hook-form";
import CartInner from "./CartInner";
function calcTotalCompoundInterest(total: any, year: any, rate: any) {
var interest = rate / 100 + 1;
return parseFloat((total * Math.pow(interest, year)).toFixed(4));
}
export default function Checkout() {
const [cart, setCart] = useState<any>([
{ price: 2000, id: 1 },
{ price: 4000, id: 2 }
]);
const [projectedGrowth, setProjectedGrowth] = useState<any>([]);
const onSubmit = async (data: any) => {};
const handleOnChange = (index: any, e: any) => {
console.log(index);
const newValue = {
price: Number(e.target.value),
id: index + 1,
property_id: index + 1
};
const newArray = [...cart];
newArray[index] = newValue;
setCart(newArray);
};
const getGrowthAmount = () => {
const newArray = [...projectedGrowth];
for (let index = 0; index < cart.length; index++) {
console.log(index);
//console.log(cart[index].price, "default values for price");
console.log(cart);
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
newArray[index] = newValue;
//newArray.splice(index, 1, newValue);
}
setProjectedGrowth(newArray);
};
// whenever the cart is updated call the getGrowthAmount function
useEffect(() => {
//for (let index = 0; index < cart.length; index++) {
getGrowthAmount();
//}
}, [cart]);
console.log(projectedGrowth, "projectedGrowth");
console.log(cart, "cart");
return (
<>
<form className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{cart.map((element, index) => {
return (
<fieldset key={index}>
<CartInner
// projectedGrowth={projectedGrowth[index]}
element={element}
handleOnChange={(e) => handleOnChange(index, e)}
defaultValues={cart[index]?.price}
/>
</fieldset>
);
})}
</div>
<div className="bg-slate-200 p-8 flex flex-col items-stretch">
<button>Proceed to payment</button>
</div>
</form>
</>
);
}
the error is on the useEffect function. In your implementation the function on all change call iteratively on the index the getGrowthAmount function that create a new array each time.
You should call only one time the getGrowthAmount function and in the cycle value the array at the index of the cycle. Then you can update pass the array to be updated.

Reaching new state in React (Hooks)

I have a small Tic tac toe game, everything is fine except I am struggling on finding where to put finish function in the code. Let me show you;
import React, { useState } from "react";
import { isGameOver } from "../Helper/helper";
import Square from "./Square";
const Board: React.FC = () => {
const [squares, setSquares] = useState(new Array(9).fill(""));
const [activePlayer, setActivePlayer] = useState("Player 1");
const [winner, setWinner] = useState<any>();
const clickSquare = (id: number): void => {
if (!squares[id]) {
if (activePlayer === "Player 1") {
setActivePlayer("Player 2");
} else {
setActivePlayer("Player 1");
}
}
setSquares((prevSquares) => {
return prevSquares.map((square, i) => {
if (i === id && !square) {
square = activePlayer === "Player 1" ? "X" : "O";
}
return square;
});
});
};
return (
<div className="board">
<div className="game-field">
{squares.map((square, i) => {
return (
<Square clickSquare={clickSquare} square={square} key={i} i={i} />
);
})}
</div>
<p
style={{
marginRight: activePlayer === "Player 1" ? "250px" : "-250px",
}}
>
{activePlayer + "'s turn"}
</p>
<p> </p>
</div>
);
};
export default Board;
This is basically the whole game and I have a finishing function in another file like this;
export const isGameOver = (squares: String[]) => {
const won = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < won.length; i++) {
const [x, y, z] = won[i];
if (squares[x] && squares[x] === squares[y] && squares[y] === squares[z]) {
return squares[x];
}
}
return null;
};
The finishing function also working fine, but since useState is working asynchoronously and I know that, I'm struggling where to put it. If I put inside of the click function it is not able to react new state, only the state just before. If I put outside with just If statement, it says too many renders cause I'm setting the winner etc.. I mean I want to also stop the game, stop clicking events etc.. Couldn't find a way how to implement this.
Seems like you need useEffect. Basically, check if the game is over, everytime a square is clicked:
useEffect(() => {
if(squares){
if(isGameOver(squares)){
setSquares(null)
/* Do other stuff */
}
}
}, [squares])
By passing squares to the dependency array, you tell react that you want this useEffect to be run everytime squares state variable changes. You can read more here

React useState to update a specific item in its array

I have an array of images inside refData. I then map them into an array of img objects with RefItem. I then want thouse specifc images to change to the next one in a line - so img 0 becomes 1, and 1 becomes 2 - as written in refbox. I just cannot figure this one out.. I just want to update the individual numbers in each array item?
import React, {useState, useEffect} from 'react'
import refData from './refData'
import RefItem from './refItem'
function ReferencesPP(){
const refItems = refData.map(item => <RefItem key={item.id} pitem={item}/>)
const [refs, setRefs] = useState([0, 1, 2, 3])
useEffect(() => {
const intervalId = setTimeout(() => {
for(let i = 0; i < refs.length; i++){
if(refs[i] !== refData.length){
setRefs(...refs[i], refs[i] = refs[i] + 1)
} else {
setRefs(...refs[i], refs[i] = 0)
}
}
}, 2000);
return () => clearTimeout(intervalId);
}, [refs]);
return(
<div className="referencespp-container">
<div className="background-container" />
<div className="content-container">
<div id="refbox">
{refItems[refs[0]]}
{refItems[refs[1]]}
{refItems[refs[2]]}
{refItems[refs[3]]}
</div>
</div>
</div>
)
}
export default ReferencesPP
I thought it would be as simple, as writing
const refs = [0, 1, 2, 3]
useEffect(() => {
const intervalId = setInterval(() => {
for(let i = 0; i < refs.length; i++){
if(refs[i] !== refData.length){
refs[i] = refs[i] + 1;
} else {
refs[i] = 0;
}
}
}, 2000);
return () => clearInterval(intervalId);
}, []);
but doing that only updates the const and not the {refItems[refs[0]]} elements?
Have a look at this https://jsfiddle.net/gwbo4pdu/ I think it's what you want to get
React.useEffect(() => {
const intervalId = setTimeout(() => {
const newRefs = [...refs]
const i = newRefs.shift()
newRefs.push(i)
setRefs(newRefs);
}, 2000);
return () => clearTimeout(intervalId);
}, [refs]);
P.S. you can do just newRefs.push(newRefs.shift())

React JS Hooks - Maximum update depth exceeded

I have the following component:-
import React, { useState, useEffect } from "react"
import Card from "./Card"
import joker from "../../assets/joker.png"
import goalie from "../../assets/goalie.png"
import defender from "../../assets/defender.png"
import midfielder from "../../assets/midfielder.png"
import attacker from "../../assets/attacker.png"
const CardsBoard = () => {
const deckLimits = {
joker: 4, goalkeepers: 12, defenders: 12, midfielders: 12, attackers: 12
};
const [ratingObj, setRatingObj] = useState({});
const [visible1, setVisible1 ] = useState(false);
const [visible2, setVisible2 ] = useState(false);
const [deck, setDeck] = useState([]);
const [deck1, setDeck1] = useState([]);
const [deck2, setDeck2] = useState([]);
const [currCardPl1, setCurrCardPl1] = useState({});
const [currCardPl2, setCurrCardPl2] = useState({});
useEffect(() => {
generateDeck();
distributeCards();
}, [deck]);
const startGame = () => {
//reset decks
setDeck([]);
setDeck1([]);x
setDeck2([]);
//generate the card’s values
generateDeck();
if (deck.length > 0) {
distributeCards();
if (currCardPl1 != undefined){
console.log(currCardPl1);
//unMask ratings of Player 1
setVisible1(true);
setVisible2(false);
}
}
};
const distributeCards = () => {
//randomize the deck
deck.sort(() => Math.random() - 0.5);
//distribute 26 cards at random when the game start
const splitDeck1 = deck.slice(0, 26);
const splitDeck2 = deck.slice(26, 52);
//add this card to the deck
splitDeck1.map((card) => {
setDeck1(deck1 => [...deck1, card]);
});
//add this card to the deck
splitDeck2.map((card) => {
setDeck2(deck2 => [...deck2, card]);
});
//queue the first card to Player 1
setCurrCardPl1(deck1[0]);
//queue the first card to Player 2
setCurrCardPl2(deck2[0]);
};
const generateDeck = () => {
for (let i = 0; i < deckLimits.joker; i++) {
generateCard('joker');
};
for (let i = 0; i < deckLimits.goalkeepers; i++) {
generateCard('goalkeeper');
};
for (let i = 0; i < deckLimits.defenders; i++) {
generateCard('defender');
};
for (let i = 0; i < deckLimits.midfielders; i++) {
generateCard('midfielder');
};
for (let i = 0; i < deckLimits.attackers; i++) {
generateCard('attacker');
};
}
const generateCard = item => {
const card = {
player: 0,
image: getImage(item),
title: item.toUpperCase(),
ratings: [
{ title: "Handling", rating: "99" },
{ title: "Reflexes", rating: "99" },
{ title: "Defending", rating: "99" },
{ title: "Strength", rating: "99" },
{ title: "Passing", rating: "99" },
{ title: "Flair", rating: "99" },
{ title: "Finishing", rating: "99" },
{ title: "Composure", rating: "99" },
],
}
//add this card to the deck
setDeck(deck => [...deck, card]);
}
const getImage = item => {
switch (item) {
case "joker":
return joker;
case "goalkeeper":
return goalie;
case "defender":
return defender;
case "midfielder":
return midfielder;
case "attacker":
return attacker;
default:
break;
}
};
return (
<div className="container-fluid justify-content-center">
<div className="row">
<div className="col-md-6">
<div>
<h4>Player 1</h4>
</div>
<Card
cardInfo={currCardPl1}
showRatings={visible1}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
<div className="col-md-6">
<h4>Player 2</h4>
<Card
cardInfo={currCardPl2}
showRatings={visible2}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
</div>
<div className="row top-buffer">
<div className="col-md-12 text-center">
<button
id="startGameBtn"
name="StartGameButton"
className="btn btn-secondary"
onClick={() => startGame()}
>
START GAME
</button>
</div>
</div>
</div>
)
}
export default CardsBoard
And I am getting the error:-
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I am suspecting it has to do something with the Submit button (onClick) which is calling the setState multiple times, however I cannot seem to fix the problem.
So I have the following questions :-
How can I fix the onClick()?
I would like to click on the Start Game button, and trigger generateDeck(); and distributeCards(); so that I get the deck, deck1 and deck2 filled up. Is there a way to do it? At the moment I have created a useEffect() which is dependant on the deck being filled up, is that the correct way to doing it with hooks?
Thanks for your help and time!
You have deck as a dependency in your useEffect hook. That means generateDeck will run every time the deck is updated. However, generateDeck calls generateCard, which calls setDeck. That causes deck to change, which means you're stuck in a sort of infinite loop. You should tie generateDeck to the onClick handler or some other event which will not be triggered by deck changing.
Also, be careful with this:
deck.sort(() => Math.random() - 0.5)
Calling sort will mutate your state directly, which is something you should avoid in React. Try something like setDeck(d=>[...d].sort(/* Your sorting function here */)) instead.
You are using setDeck() in distrubuteCards and in your useEffect hook, you call again the distributeCards
const distributeCards = () => {
// bla bla
setDeck(deck => [...deck, card]);
}
Which results in calling the hook all the time.
useEffect(() => {
generateDeck();
distributeCards();
}, [deck]);
deck.sort(() => Math.random() - 0.5);
You have a problem here.
Save it inside a variable. Otherwise you update the state all the time it's called.

Categories

Resources