React setState and use the value to update another state - reactjs

I have a simple roll the dice code and I want to update the score based on the dice roll.
This is the states of the dices and the scores:
const [Dice1, setDice1] = useState(0);
const [Dice2, setDice2] = useState(0);
const [Score1, setScore1] = useState(0);
const [Score2, setScore2] = useState(0);
function randomNum() {
setDice2(Math.floor(Math.random() * 6) + 1);
setDice1(Math.floor(Math.random() * 6) + 1);
Dice1 > Dice2 && Dice1 !== Dice2 ? setScore1(Score1 + 1) : Dice2 > Dice1 && Dice1 !== Dice2 ? setScore2(Score2 + 1) : setScore2(Score2 + 0);
}
The function randomNum is triggered on click.
return (
<div className="App">
<header className="App-header">
<p>Player 1 rolls {Dice1}</p>
<p>Player 2 rolls {Dice2}</p>
<button onClick={randomNum}>Roll the dice</button>
{Dice1 > Dice2 ? <p>Player 1 win</p> : Dice1 === Dice2 ? <p>Draw</p> : <p>Player 2 win</p>}
<p>Player 1 score is {Score1} points</p>
<p>Player 2 score is {Score2} points</p>
<button onClick={resetScore}>Reset score</button>
</header>
</div>
);
Everything works well, except that the score if updated with 1 round lag.
Everytime I roll, it is added the point from the last round.
What am I'm doing wrong here?

Update
One Score state would look like
const [score, setScore] = useState({
score1: 0,
score2: 0
});
You can update like
setScore(prev=>{...prev, score1: prev.score1 + 1})
setScore(prev=>{...prev, score2: prev.score2 + 1})
Try this
Use one state for dice instead
const [dice, setDice] = useState({
dice1: 0,
dice2: 0,
})
Update the state
function randomNum() {
const dice1Val = Math.floor(Math.random() * 6) + 1;
const dice2val = Math.floor(Math.random() * 6) + 1;
setDice({
dice1: dice1Val,
dice2val: dice2Val
})
And since setting score is an effect of setting dice, update the score in a useEffect
useEffect(() => {
dice.dice1 > dice.dice2 && dice.dice1 !== dice.dice2 ? setScore1(prev => prev + 1); : dice.dice2 > dice.dice1 && dice.dice1 !== dice.dice2 ? setScore2(prev => prev + 1) : setScore2(prev => prev + 1)
},[dice])
}
You can also put the scores under one state instead.

The most easiest way I can think of is to implement the resetScore function that will set the both states to 0.
function resetScore() { setScore1(0); setScore2(0); }
or call this function at the start of the randomNum.

You are not using the previous state values correctly. It is not recommended to use previous state values directly while updating state variables.
So in your code instead of :
setScore1(Score1 + 1)
you should do like this :
setScore1(prevScore => prevScore + 1);
and similarly for other state variables.

Related

React SVG Component Not Always Updating

I'm currently having an SVG object composed of a list of "blocks" that changes based on a Select selection. The problem that I have is that the shape of the blocks only appear to update when the number of blocks in the selected plan is different.
So, for example, if I start with PlanA and it consists of 2 blocks, selecting PlanB with 3 blocks will give me the result I expect. But if I then select PlanC that also has 3 blocks, the dimensions of these blocks doesn't change (though the accompanying text does).
An example for what I've done with the top level component:
const [planOptions, setPlanOptions] = useState([]); //would be filled with option info
const [selectedOption, setSelectedOption] = useState(null);
const [fullInfo, setFullInfo] = useState([]); //would contain the full selection of information
const [blocks, setBlocks] = useState([]);
<PlanChartArea
key={blocks}
blocks={blocks}
/>
function selectPlan(fullInfo, planVal) {
//get blocks by chosing correct plan and ignoring the initial "marker" element
let blockList = fullInfo.filter(elem => elem.plan == planVal && elem.point > 1);
//
////////
//----PERFORM TRANSFORMATIONS
////////
//
setBlocks(blockList);
}
const handleSelectChange = (option) => {
setSelectedOption(option);
var selection = option.value;
selectPlan(fullInfo, selection);
}
<Select
value={selectedOption}
placeholder="Select Plan..."
options={planOptions}
onChange={(e) => { handleSelectChange(e) }}
/>
And with the child component:
for (let i = 0; i < props.blocks.length; i++) {
blocksobj.push(<Block
key={i + "_bl"}
x1={props.blocks[i].x1pos}
x2={props.blocks[i].x2pos}
y1={props.blocks[i].y1pos}
y2={props.blocks[i].y2pos}
y0={y0}
maxYValue={maxYValue}
yLength={yLength}
/>);
blocktxt.push(<text
key={i + "_bt"}
x={asPercent(x1pos + 0.5 * (x2pos - x1pos))}
y={asPercent(y0 + 0.05 * yLength)}
fontWeight="bold"
dominantBaseline="auto"
textAnchor="middle"
>
{Math.round(props.blocks[i].value)/100}kg/min
</text>);
}
return (
<div style={{position:"parent", width:"100%", height:"100%"}}>
<svg width={asPercent(SVG_WIDTH)} height={asPercent(SVG_HEIGHT)}>
{blocksobj}
{blocktxt}
<XYAxis
xAxisLength={xLength}
yAxisLength={yLength}
ox={x0}
oy={y0}
/>
{xTickmarks}
{yTickmarks}
</svg>
</div>
);
I tried implementing the accepted solution from this question but to no effect. Perhaps this is because my state value is in the form of an Array? Any help would be much appreciated.

ReactJS - I implement Binary Search Function, it works only first time

ReactJS - I implement Binary Search Function, it works only first time but after I change the value in the input box, it always return -1 even it has the value in the Array.
Please see the following code:
import React, { useState } from 'react'
import { Container } from 'react-bootstrap'
const binarysearch = () => {
const [ insertValue, setInsertValue ] = useState(0)
var exarr = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]
// Binary Search
const binarySearch = (arr, val) => {
let start = 0, end = arr.length - 1
while (start <= end) {
let mid = Math.floor((start + end)/2)
console.log(mid)
if (arr[mid] === val) {
return mid
}
if (val < arr[mid]) {
end = mid - 1
} else {
start = mid + 1
}
}
return -1
}
// End Binary Search
return (
<div>
<br />
<hr />
<Container>
<h1>Binary Search</h1>
<h4>Array = {JSON.stringify(exarr)}</h4>
<h4>Search <input type="number" onChange={e => setInsertValue(e.target.value)} /></h4>
<h3>Search {insertValue}, Result in location: {binarySearch(exarr,insertValue)}</h3>
</Container>
<hr />
</div>
)
}
export default binarysearch
First Time Load
After Change Input (Search 10 it should return 10 but -1)
The problem is the fact that e.target.value is always a string, even when the input type attribute is set to "number".
So, when you do arr[mid] === val it will be always false, since this is comparing a number to a string.
You can see this behaviour here.
To fix this, do onChange={e => setInsertValue(Number(e.target.value))}.
Or, alternatively, you can use the non strict equality comparison, which is not really recommended, by replacing the === operator by just ==.
Thank you very much #Mario Vernari
I update the below line to change from string to number, it works properly.
(Insert '+' to insertValue)
From
<h3>Search {insertValue}, Result in location: {binarySearch(exarr,insertValue)}</h3>
To
<h3>Search {insertValue}, Result in location: {binarySearch(exarr, +insertValue)}</h3>

Conditional unique rendering

I have an array with objects sorted with a date entry and a component to list in what approximated time it was listed.
const TimeHeader = ({date}) => {
const calculateDays = (date) => {
let timeDiff = new Date().getTime() - new Date(date).getTime();
let days = Math.ceil(timeDiff / (1000 * 3600 * 24));
return days;
}
let daysGone = calculateDays(date);
let color = "green";
let text = "Less than 1 Week";
if (daysGone > 7 && daysGone <= 30){
color = "blue";
text = "Less than 1 Month";
} else if (daysGone > 30 && daysGone <= 90){
color = "yellow";
text = "Less than 3 Months";
} else if (daysGone > 90){
color = "red";
text = "More than 3 Months";
}
return (
<div style={{backgroundColor: color}}>{text}</div>
)
}
How would I call upon this component to be rendered only once for each instance? (Once for Less than 1 week, once for less than one month etc)
Right now I am just calling it before each item listing but of course it leads a lot of repetition.
{list.map(item => {
return (
<div key={item.id}>
<TimeHeader date={item.date} />
<ItemDisplay item={item} />
</div>
)
})}
One solution would be to split the array to different categories beforehand, but I'm wondering if there is a nice solution that doesn't require splitting the array.

setState not applying changes in React function component

Im trying to do a Pomodoro Clock timer, which is basically two timers that alternate between.
Thats all the code:
import React, { useState } from "react";
function Challenge20() {
const [timer, setTimer] = useState('');
let minutes = 0;
let seconds = 0;
const [workRest, setWorkRest] = useState('work');
function startTimer() {
document.getElementById('start').style.display = 'none';
minutes = document.getElementById('work').value - 1;
seconds = 59;
setInterval(reduceSeconds, 1000);
};
function reduceSeconds() {
if (seconds < 10) {
setTimer(minutes + ':' + '0' + seconds);
}
else {
setTimer(minutes + ':' + seconds);
}
seconds -= 1;
if (seconds < 1 && minutes > 0) {
seconds = 59;
minutes -= 1;
}
else if (seconds == 0 && minutes == 0){
setWorkRest(workRest == 'work' ? 'rest' : 'work');
minutes = document.getElementById(workRest == 'work' ? 'work' : 'rest').value;
}
};
return (
<>
<label>Work Minutes:</label>
<input id='work' type='number' max='60'/>
<br/>
<label>Rest Minutes:</label>
<input id='rest' type='number' max='60'/>
<br/>
<br/>
<span id='timer'>{workRest} -> {timer}</span>
<button id='start' onClick={() => startTimer()}>Start!</button>
</>
);
};
export default Challenge20;
The problem is in this part:
else if (seconds == 0 && minutes == 0){
setWorkRest(workRest == 'work' ? 'rest' : 'work');
minutes = document.getElementById(workRest == 'work' ? 'work' : 'rest').value;
}
The setState is not changing from 'work' to 'rest', also tried to call a function to change the state, clearing interval and 2 separated if, nothing worked, what am I doing wrong?
useState is not work inside the condition. For ex: you are set the state value in the if condition. State value not updated in condition.
I think this is what you're trying to achieve? The problem is that the timer keeps going. On the next iteration, it sets workRest back to its previous value. To solve this, I used clearInterval to stop iterating, and decremented seconds to display 00:00 on the timer. As such, I had to assign the interval creation to a variable we can pass into clearInterval.
import React, { useState } from "react";
function Challenge20() {
const [timer, setTimer] = useState("");
let minutes = 0;
let seconds = 0;
const [workRest, setWorkRest] = useState("work");
let interval;
function startTimer() {
document.getElementById("start").style.display = "none";
minutes = document.getElementById("work").value - 1;
seconds = 59;
interval = setInterval(reduceSeconds, 1);
}
function reduceSeconds() {
if (seconds < 10) {
setTimer(minutes + ":" + "0" + seconds);
} else {
setTimer(minutes + ":" + seconds);
}
seconds -= 1;
if (seconds < 1 && minutes > 0) {
seconds = 59;
minutes -= 1;
} else if (seconds == 0 && minutes == 0) {
console.log();
setWorkRest(workRest == "work" ? "rest" : "work");
minutes = document.getElementById(workRest == "work" ? "work" : "rest")
.value;
clearInterval(interval);
seconds -= 1;
}
}
return (
<>
<label>Work Minutes:</label>
<input id="work" type="number" max="60" />
<br />
<label>Rest Minutes:</label>
<input id="rest" type="number" max="60" />
<br />
<br />
<span id="timer">
{workRest} -> {timer}
</span>
<button id="start" onClick={() => startTimer()}>
Start!
</button>
</>
);
}

How to grab text values from getAllByTestId?

I have a component that lists a bunch of items. In this, they're cryptocurrency assets. When I click the header labelled Name, it sorts the items in alphabetical order. I would like to test this functionality by clicking the Name button in order to fire the event, and then asserting that the values in the name column are sorted alphabetically:
it("Sorts by name on click", () => {
const sortedByName = spotData
.sort((a, b) => {
return a.name < b.name ? -1 : a.name === b.name ? 0 : 1;
})
.map((ticker) => ticker.name);
const { getByText, getAllByTestId } = renderWithProviders(
<MarketSelectorPanel marketsList={spotData} />
);
fireEvent.click(getByText("Name"));
expect(getAllByTestId("market-selector-row-name")).toEqual(
sortedByName
);
});
The above doesn't work because expect(getAllByTestId("market-selector-row-name")) grabs the entire HTML element:
● MarketSelectorPanel tables › Sorts by name on click
expect(received).toEqual(expected) // deep equality
- Expected
+ Received
Array [
- "BCH/USD",
- "BTC/USD",
- "ETH/USD",
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ BCH/USD
+ </span>,
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ BTC/USD
+ </span>,
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ ETH/USD
+ </span>
]
Please try passing the XPath of the component for testing rather than the id
Solved using getNodeText:
const columnAfterClick = getAllByTestId("market-selector-row-name").map(getNodeText); // Contains array of text strings only now
expect(columnAfterClick).toEqual(sortedByName);

Resources