React multiple useState hooks in event handler behaving in an erroneous way - reactjs

I have coded a simple React application that tracks feedback, the whole code for the application is below:
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
const App = () => {
// save clicks of each button to its own state
const [good, setGood] = useState(0)
const [neutral, setNeutral] = useState(0)
const [bad, setBad] = useState(0)
const [all, setAll] = useState(0)
const [average, setAverage] = useState(0)
const [positive, setPositive] = useState(0)
const handleGood = () => {
setGood(good + 1)
setAll(all + 1)
setAverage((average + 1) /(all))
setPositive(good/all)
}
const handleNeutral = () => {
setNeutral(neutral + 1)
setAll(all + 1)
setAverage((average) / (all))
}
const handleBad = () => {
setBad(bad + 1)
setAll(all + 1)
setAverage((average - 1) / (all))
}
return (
<div>
<h1>give feedback</h1>
<button onClick={handleGood}>good</button>
<button onClick={handleNeutral}>neutral</button>
<button onClick={handleBad}>bad</button>
<h3>statistics</h3>
<div>good {good}</div>
<div>neutral {neutral}</div>
<div>bad {bad}</div>
<div>all {all}</div>
<div>average {average}</div>
<div>positive {positive}</div>
</div>
)
}
ReactDOM.render(<App />,
document.getElementById('root')
)
From the initial state if I click the "good" button once, the AVERAGE comes out to Infinity and POSTIVE comes out to Nan, I'm really confused why the application is behaving like this, is this because I'm using multiple setState methods in the click event handlers, and they are synchronous functions? Is there an important concept that I'm missing here when using useState hooks.

and they are synchronous functions
Sort of - they don't return promises, but when you do:
setAll(all + 1)
setAverage((average) / (all))
all won't have updated until the next render cycle. What you need to do is more like:
const newAll = all + 1
setAll(newAll);
setAverage(average/newAll);
if you want to use the updated value immediately after setting it.

Related

Call function when time changes in react

Being new to react , this all is really confusing and new to me , so I apologise if I'm making some obvious oversight.
Im making a stopwatch and implementing the seconds for starters. However; Im confused as to how i'll implement the on display seconds number to update when each second passes.
This is what I'm doing right now
function App() {
const [time , updateTime] = React.useState(0);
var startsec = 0;
//UpdateTime should get triggered when next second passes
const UpdateTime = () => {
//Update time variable with the new seconds elapsed
}
//Should run every second or something
const CheckTimeUpdation = () => {
currentsec = Math.floor(Date.now() / 1000.0);
console.log(currentsec);
if(currentsec > startsec){
UpdateTime(currentsec-startsec);
}
}
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
}
//Clock component just gets a number and displays it on the screen
return (<div className = "App">
<Clock timerSeconds= {time}/>
<div>
<button onClick={GetStartTime}></button>
</div>
</div>);
}
export default App;
Date.now() function gets the miliseconds passed since 1970 (hence the division by 1000 to make them into seconds) and I find the difference between when the button was clicked and current one and passs that to the time component to display.
How do I make the CheckTimeUpdation function run every second or so?
What you want is the setInterval() method (see: https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
However your code so far has some issues:
On the button click, getStartTime runs and it updates the value of startsec. Firstly, this does not cause the component to re-render, and so the component will not update and you will see nothing changing on your screen. Also, if you did get your component to re-render, you will notice that startsec will be 0 again on the next re-render, so re-assigning startsec like how you did likely doesn't do what you want it to. If you want to persist values between rerenders, you can use useState (https://reactjs.org/docs/hooks-reference.html#usestate) or useRef (https://reactjs.org/docs/hooks-reference.html#useref).
Now i'm assuming you want to start the timer when the button is clicked. What you need is to start the interval (via setInterval) on the button click, and update time every 1000ms.
You'd also want to clear the interval once you don't need it anymore, using the clearInterval() method. You'll need to save the id returned from setInterval() initially in order to clear it later.
Below I have written a short example using this idea with setInterval on button click to help you:
import { useState, useRef } from "react";
export default function App() {
const [timerState, setTimerState] = useState("paused");
const [timeElapsed, setTimeElapsed] = useState(0);
const intervalId = useRef(0);
const isRunning = timerState === "running";
const isPaused = timerState === "paused";
const onStartTimer = () => {
intervalId.current = setInterval(() => {
setTimeElapsed((time) => time + 1);
}, 1000);
setTimerState("running");
};
const onStopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = 0;
setTimerState("paused");
};
const onClearTimer = () => {
setTimeElapsed(0);
};
return (
<div className="App">
<h1>{timeElapsed}</h1>
{isPaused && <button onClick={onStartTimer}>Start timer</button>}
{isRunning && <button onClick={onStopTimer}>Stop timer</button>}
{!!timeElapsed && <button onClick={onClearTimer}>Clear timer</button>}
</div>
);
}
You can use setInterval in the GetStartTime Function.
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
setInterval(() => CheckTimeUpdation(), 1000);
}

Pomodoro Timer REACTJS not working - useState not updating

I'm quite new to ReactJS and have been working on a Pomodoro Timer. How it works is that whenever the "Work Timer" reaches zero, it switches to the "Break Timer".
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
However,
this line: setSecondsLeft(nextSeconds); // This does not update secondsLeft
under useEffect, does not update the secondsLeft which results in the timer for the break to display wrongly.
Could use some advice to make this work.
Thanks!
import {
Button,
Heading,
VStack,
Stack,
HStack,
Box,
CircularProgress,
Text,
CircularProgressLabel,
} from '#chakra-ui/react';
import { useState, useRef, useContext, useEffect } from 'react';
import { SettingsContext } from '../helpers/SettingsContext';
import PomodoroSettings from './PomodoroSettings';
// Pomodoro
const Pomodoro = () => {
//Timer
const { newTimer, setNewTimer } = useContext(SettingsContext);
const [isPaused, setIsPaused] = useState(true);
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
const secondsLeftRef = useRef(secondsLeft);
const isPausedRef = useRef(isPaused);
const modeRef = useRef(newTimer.active);
function tick() {
secondsLeftRef.current--;
setSecondsLeft(secondsLeftRef.current);
}
useEffect(() => {
function switchMode() {
const nextMode = modeRef.current === 'work' ? 'break' : 'work';
const nextSeconds =
(nextMode === 'work' ? newTimer.work : newTimer.short) * 60;
setNewTimer({
work: newTimer.work,
short: newTimer.short,
long: newTimer.long,
active: nextMode,
});
modeRef.current = nextMode;
console.log(nextMode);
console.log('Next: ' + nextSeconds);
setSecondsLeft(nextSeconds); // This does not update secondsLeft
console.log('SecondsLeft: ' + secondsLeft);
secondsLeftRef.current = nextSeconds;
}
secondsLeftRef.current = newTimer.work * 60;
setSecondsLeft(secondsLeftRef.current);
const interval = setInterval(() => {
if (isPausedRef.current) {
return;
}
if (secondsLeftRef.current === 0) {
return switchMode();
}
tick();
}, 1000);
return () => clearInterval(interval);
}, [newTimer, setNewTimer]);
const totalSeconds =
newTimer.active === 'work' ? newTimer.work * 60 : newTimer.short * 60;
const percentage = Math.round((secondsLeft / totalSeconds) * 100);
const minutes = Math.floor(secondsLeft / 60);
let seconds = secondsLeft % 60;
if (seconds < 10) seconds = '0' + seconds;
// Start / Pause
const handleButton = (e) => {
e.preventDefault();
if (isPaused) {
setIsPaused(!isPaused);
isPausedRef.current = false;
} else {
setIsPaused(!isPaused);
isPausedRef.current = true;
}
};
// console.log(percentage)
// console.log(newTimer.active)
// console.log(secondsLeft)
// console.log(totalSeconds)
return (
<div>
<Heading as='h4' size='md'>
{' '}
Pomodoro{' '}
</Heading>
<VStack>
<HStack>
<Button variant='ghost'>Pomodoro</Button>
<Button variant='ghost'>Short Break</Button>
<Button variant='ghost'>Long Break</Button>
</HStack>
<CircularProgress
value={percentage}
color={newTimer.active === 'work' ? 'red.400' : 'green.400'}
size='200px'
thickness='10px'
>
<CircularProgressLabel>
<Text fontSize='3xl'>{minutes + ':' + seconds}</Text>
</CircularProgressLabel>
</CircularProgress>
<HStack>
<Button variant='outline' onClick={handleButton}>
{isPaused ? <div> START </div> : <div> STOP </div>}
</Button>
<PomodoroSettings />
</HStack>
</VStack>
</div>
);
};
export default Pomodoro;
useState uses array const [value, setValue] like this. It might be just an error in this declaration of newTimer.
There is too much going on in one eseEffect(). The initial value of newTimer.work is not modified so each re-render will set secondsLeftRef to the same initial value.
Since useEffect()updates a context object, the component will re-render on every iteration, setting secondsLeftRef to its initial value.
Not sure where the problem is but few things I noticed that you can try to fix or improve in your code -
Do not use or assign context or props value in useState as you are doing it for secondsLeft, reason is that useState is supposed to run once when component gets rendered first time and you might not get value for newTimer.work on very first render.
For this you can use useEffect and newTimer in dependency array.
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
You are trying to log secondsLeft immediatly after calling setSecondsLeft this will not give you correct value because setting state is asynchronous in react so it will not be available on next line after calling set state.
setSecondsLeft(nextSeconds);
console.log('SecondsLeft: ' + secondsLeft);
One last thing I noticed looking at your code is that it's possible that you might be registering multiple setInterval because below line is responsible for clearing interval only when component gets unmount but your useEffect will get called multiple times based on its dependencies.
return () => clearInterval(interval);
Try to debug your code for above 3rd point and if that's the problem you
can try clearing your interval in start of useEffect or add
some conditions to make sure it will get register once to get
expected results.
Note - I have not tried executing your code, but let me know if this helps or if I can improve my answer in any way.

Redux useDispatch Hook on click event of nodeListOf<HTMLElement>

I have a component that renders a grid. I'm trying to count the moves made (onclick of each grid box).
But when I include dispatch on the eventListener it returns an error. The moveCharacter function is supposed to move the character around those boxes and its working well. I just need a way to be able to count the moves made (onclick of each box) and store in general state to use in another component.
function GridBoxes():JSX.Element{
const gridValue: number = useSelector<IStateProps, IStateProps["grid"]>((state)=> state.grid);
const totalMoves: number = useSelector<IStateProps, IStateProps["totalMoves"]>((state)=> state.totalMoves);
const history = useHistory();
const dispatch = useDispatch();
useEffect(()=>{
const boxElements = document.querySelectorAll('.box');
boxElements.forEach(element => {
element.addEventListener('click', (e)=>{
moveCharacter(element.id, getCharacterPosition(boxElements));
dispatch({type: "COUNT_MOVES", totalMoves: totalMoves + 1});
console.log("moves");
});
});
});
useEffect(()=> {
let t = setInterval(()=> {
const timeSpent = document.getElementById("time-spent");
const indicator = document.getElementById("indicator");
let countDown = Number(timeSpent?.innerHTML);
countDown = countDown - 1;
let timeTakenPercent = ((gridValue*3) - countDown) / (gridValue*3) * 100;
dispatch({type:"SET_TIME", payload: (gridValue*3)-countDown});
(indicator as any).style.width = timeTakenPercent+"%";
(timeSpent as any).innerHTML = countDown.toString().length < 2 ? countDown.toString().padStart(2,"0") : countDown;
if(countDown < 1){
clearInterval(t);
history.push("/over");
play("https://freesound.org/data/previews/175/175409_1326576-lq.mp3");
}
}, 1000);
});
const [emptyBox, characterBox, foodBox]: string[] = ['<div class="box"></div>',`<div class="box"><img src=${assets.character} /></div>`, `<div class="box"><img src=${assets.food} /></div>`];
const generatedGrids: string[][] = gridPattern({grid: gridValue, box:emptyBox, character: characterBox, food: foodBox});
return (
<>
{generatedGrids.map((box, i)=> {
box = setElementId(box,i);
return <div key={i} className="col">{ReactHtmlParser(box.join(" "))}</div>;
})}
</>
);
}
export default GridBoxes;
Error gotten
The error does not really come from Redux, but from some manual DOM manipulation you are doing.
Your dispatch call only surfaces it: when calling dispatch, redux state will change which will trigger a React rerender - while you are manually fiddling around with the DOM and the two things collide.
My question is: why do you do all this? The whole point of React is that is builds the DOM for you and attaches event handlers for you. At no point should you be using something like react-html-parser, manually concatenate html strings, manually call addEventListener, modify innerHTML, style or anything else on DOM elements.
Let React build your DOM for you and this error will go away.

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

useState() is not updating state from event handler?

I'm trying to recreate an old flash game in React. The object of the game is to press a button down for a certain length of time.
This is the old game:
http://www.zefrank.com/everysecond/index.html
Here is my new React implementation:
https://codesandbox.io/s/github/inspectordanno/every_second
I'm running into a problem. When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released, using the Moment.js time library. If the timeDifference between the onMouseDown and onMouseUp event is within the targetTime, I want the game level to increase and the targetTime to increase as well.
I'm implementing this logic in the handleMouseUp event handler. I'm getting the expected times printed to the screen, but the logic isn't working. In addition, when I console.log() the times, they are different than the ones being printed to the screen. I'm fairly certain timeHeld and timeDifference aren't being updated correctly.
Initially I thought there was a problem with the way I was doing the event handler and I need to use useRef() or useCallback(), but after browsing a few other questions I don't understand these well enough to know if I have to use them in this situation. Since I don't need access to the previous state, I don't think I need to use them, right?
The game logic is in this wrapper component:
import React, { useState } from 'react';
import moment from 'moment';
import Button from './Button';
import Level from './Level';
import TargetTime from './TargetTime';
import TimeIndicator from './TimeIndicator';
import Tries from './Tries';
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(.2);
const [isPressed, setIsPressed] = useState(false);
const [whenPressed, setPressed] = useState(moment());
const [whenReleased, setReleased] = useState(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = whenReleased.diff(whenPressed) / 1000;
let timeDifference = Math.abs(targetTime - timeHeld);
timeDifference = Math.round(1000 * timeDifference) / 1000; //rounded
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
setPressed(moment());
};
const handleMouseUp = () => {
setIsPressed(false);
setReleased(moment());
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
return (
<div>
<Level level={level}/>
<TargetTime targetTime={targetTime} />
<Button handleMouseDown={handleMouseDown} handleMouseUp={handleMouseUp} isGameOver={gameOver} />
<TimeIndicator timeHeld={timeHeld} timeDifference={timeDifference} isPressed={isPressed} gameStarted={gameStarted} />
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
)
}
export default TimerApp;
If you want to check the whole app please refer to the sandbox.
If you update some state inside a function, and then try to use that state in the same function, it will not use the updated values. Functions snapshots the values of state when function is called and uses that throughout the function. This was not a case in class component's this.setState, but this is the case in hooks. this.setState also doesn't updates the values eagerly, but it can update while in the same function depending on a few things(which I am not qualified enough to explain).
To use updated values you need a ref. Hence use a useRef hook. [docs]
I have fixed you code you can see it here: https://codesandbox.io/s/everysecond-4uqvv?fontsize=14
It can be written in a better way but that you will have to do yourself.
Adding code in answer too for completion(with some comments to explain stuff, and suggest improvements):
import React, { useRef, useState } from "react";
import moment from "moment";
import Button from "./Button";
import Level from "./Level";
import TargetTime from "./TargetTime";
import TimeIndicator from "./TimeIndicator";
import Tries from "./Tries";
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(0.2);
const [isPressed, setIsPressed] = useState(false);
const whenPressed = useRef(moment());
const whenReleased = useRef(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = useRef(null); // make it a ref instead of just a variable
const timeDifference = useRef(null); // make it a ref instead of just a variable
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
whenPressed.current = moment();
};
const handleMouseUp = () => {
setIsPressed(false);
whenReleased.current = moment();
timeHeld.current = whenReleased.current.diff(whenPressed.current) / 1000;
timeDifference.current = Math.abs(targetTime - timeHeld.current);
timeDifference.current = Math.round(1000 * timeDifference.current) / 1000; //rounded
console.log(timeHeld.current);
console.log(timeDifference.current);
if (timeDifference.current <= 0.1) {
setLevel(level + 1);
setTargetTime(targetTime + 0.2);
} else if (timeDifference.current > 0.1 && tries >= 1) {
setTries(tries - 1);
// consider using ref for tries as well to get rid of this weird tries === 1 and use tries.current === 0
if (tries === 1) {
setGameOver(true);
}
}
};
return (
<div>
<Level level={level} />
<TargetTime targetTime={targetTime} />
<Button
handleMouseDown={handleMouseDown}
handleMouseUp={handleMouseUp}
isGameOver={gameOver}
/>
<TimeIndicator
timeHeld={timeHeld.current}
timeDifference={timeDifference.current}
isPressed={isPressed}
gameStarted={gameStarted}
/>
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
);
};
export default TimerApp;
PS: Don't use unnecessary third party libraries, especially big ones like MomentJs. They increase your bundle size significantly. Use can easily get current timestamp using vanilla js. Date.now() will give you current unix timestamp, you can subtract two timestamps to get the duration in ms.
Also you have some unnecessary state like gameOver, you can just check if tries > 0 to decide gameOver.
Similarly instead of targetTime you can just use level * .2, no need to additional state.
Also whenReleased doesn't needs to be a ref or state, it can be just a local variable in mouseup handler.
State updaters can take a value indicating the new state, or a function that maps the current state to a new state. The latter is the right tool for the job when you have state that depends on mutations of itself.
This should work if you update places in the code where you use the pattern
[value, setValue ] = useState(initial);
...
setValue(value + change);
to
[value, setValue ] = useState(initial);
...
setValue((curValue) => curValue + change);
For example,
if (timeDifference <= .1) {
setLevel((curLevel) => curLevel + 1);
setTargetTime((curTarget) => curTarget + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries((curTries) => {
const newTries = curTries - 1;
if (newTries === 1) {
setGameOver(true);
}
return newTries;
});
}
I think there are two subtle things going on here:
When you call a setState method (e.g. setRelease(moment())) the value of the associated variable (e.g. whenReleased) does not update immediately. Instead it queues a re-render, and only once that render happens will the value be updated.
The event handlers (e.g. handleMouseUp) are closures. Meaning they capture the values from the parent scope. And again therefor are only updated on a re-render. So, when handleMouseUp runs, timeDifference (and timeHeld) will be the value that was calculated during the last render.
The changes you therefore need to make are:
Move the calculation of timeDifference inside your handleMouseUp event handler.
Instead of using whenReleased in your timeDifference calculation, you need to use a local variable set to moment() (You can also set whenReleased via setReleased, but that value won't be available to you inside your event handler).
const handleMouseUp = () => {
const released = moment();
setIsPressed(false);
setReleased(released);
const timeHeld = released.diff(whenPressed) / 1000;
const timeDifference = Math.round(1000 * Math.abs(targetTime - timeHeld)) / 1000;
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released...
This is not true ... but can be ... just move the time difference calulations into handleMouseUp()
... also - you don't need whenReleased

Resources