Countdown react timer sets time to zero - reactjs

The goal is to implement a timer with a start, pause/resume, and reset buttons and also with custom input for getting minutes and seconds.
As the requirement states, the timer should not change when tying the input field, the timer should change only when start button is pressed (it show set state only after start is pressed).
Code:
const START_DERATION = 10;
function Timer() {
const [min, setMin] = useState("00");
const [sec, setSec] = useState("00");
const [currentMinutes, setMinutes] = useState("00");
const [currentSeconds, setSeconds] = useState("00");
const [isStop, setIsStop] = useState(false);
const [duration, setDuration] = useState(START_DERATION);
const [isRunning, setIsRunning] = useState(false);
const formatTime = async () => {
setMinutes(min);
setSeconds(sec);
};
const startHandler = async () => {
setDuration(
parseInt(currentSeconds, 10) + 60 * parseInt(currentMinutes, 10)
);
setIsRunning(true);
};
const stopHandler = () => {
setIsStop(true);
setIsRunning(false);
};
const resetHandler = () => {
setMinutes("00");
setSeconds("00");
setIsRunning(false);
setIsStop(false);
setDuration(START_DERATION);
};
const resumeHandler = () => {
let newDuration =
parseInt(currentMinutes, 10) * 60 + parseInt(currentSeconds, 10);
setDuration(newDuration);
setIsRunning(true);
setIsStop(false);
};
const startAll = async () => {
await formatTime();
startHandler();
};
useEffect(() => {
if (isRunning === true) {
let timer = duration;
var minutes, seconds;
const interval = setInterval(function () {
if (--timer <= 0) {
resetHandler();
} else {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
setMinutes(minutes);
setSeconds(seconds);
}
}, 1000);
return () => clearInterval(interval);
}
}, [isRunning]);
useEffect(() => {
console.log(currentMinutes);
}, [currentMinutes]);
useEffect(() => {
console.log(currentSeconds);
}, [currentSeconds]);
return (
<div>
<span style={{ display: "flex" }}>
<input type="number" onChange={(e) => setMin(e.target.value + "")} />{" "}
<p>Minutes</p>
</span>
<span style={{ display: "flex" }}>
<input type="number" onChange={(e) => setSec(e.target.value + "")} />
<p>Seconds</p>
</span>
<button onClick={formatTime}>Format</button>
<button onClick={startAll}>Start</button>
<button
onClick={isStop ? resumeHandler : stopHandler}
disabled={!isRunning && !isStop}
>
Pause/Resume
</button>
<button onClick={resetHandler} disabled={!isRunning && !isStop}>
Reset
</button>
<p>
{currentMinutes}:{currentSeconds}
</p>
</div>
);
}
But in the above case, the timer is set to 0 when the start is pressed.
Codesandbox : https://codesandbox.io/s/lively-grass-6o50xx?file=/src/Timer.js

Related

React Guarantee that part of function runs after DOM updates

Currently I have a textarea like this:
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
id={id}
value={content}
></textarea>
I am implementing some buttons to add markdown to the textarea to make it easier for the user to update and have this function for bold:
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
// Want this to run after textarea gets updated
textAreaRef.current?.focus();
textAreaRef.current?.setSelectionRange(
content.length - 3,
content.length - 3
);
}
const changeEvent = new Event('change', { bubbles: true });
// Want to run this after textarea is updated
textAreaRef.current?.dispatchEvent(changeEvent);
}, [content]);
setContent is the setter for content which is passed to the textarea. Is there a way to guarantee the parts I've marked with comments as wanting to only run once the DOM gets updated run when I want them to?
I finagled around with things and went with this approach (gonna post the entire component, which contains some stuff irrelevant to the question):
const MarkdownTextArea = ({
value,
onBlur = () => {},
onChange = () => {},
touched = false,
error,
id,
label
}: MarkdownTextAreaProps) => {
const [content, setContent] = useState(value ?? '');
const [numberOfRows, setNumberOfRows] = useState(5);
const [numberOfCols, setNumberOfCols] = useState(20);
const [isPreview, setIsPreview] = useState(false);
const [changed, setChanged] = useState<'bold' | null>();
const textAreaRef = useRef<HTMLTextAreaElement>();
useEffect(() => {
const setColsAndRows = () => {
const newColumnsNumber = Math.floor(
(textAreaRef.current?.offsetWidth ?? 100) /
(convertRemToPixels(1.2) / 1.85)
);
setNumberOfCols(newColumnsNumber);
setNumberOfRows(calculateNumberOfRows(content, newColumnsNumber));
};
setColsAndRows();
window.addEventListener('resize', setColsAndRows);
return () => {
window.removeEventListener('resize', setColsAndRows);
};
}, [content]);
const handleTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> =
useCallback(
event => {
onChange(event);
setContent(event.target.value);
setNumberOfRows(
calculateNumberOfRows(
event.target.value,
textAreaRef.current?.cols ?? 20
)
);
},
[onChange]
);
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
}
setChanged('bold');
}, []);
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}
return (
<div className={classes.container} data-testid="markdown-text-area">
<div className={classes['header']}>
<label className={classes.label} htmlFor={id}>
{label}
</label>
<Button
positive
style={{ justifySelf: 'flex-end' }}
onClick={() => setIsPreview(prev => !prev)}
>
{isPreview ? 'Edit' : 'Preview'}
</Button>
</div>
<div className={classes['text-effect-buttons']}>
<button
className={classes['text-effect-button']}
onClick={handleBoldClick}
type="button"
style={{ fontWeight: 'bold' }}
>
B
</button>
</div>
{isPreview ? (
<div className={classes['markdown-container']} id={id}>
<MarkdownParser input={content} />
</div>
) : (
<textarea
onChange={handleTextAreaChange}
className={`${classes['text-input']}${
error && touched ? ` ${classes.error}` : ''
}`}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
rows={numberOfRows}
cols={numberOfCols}
onBlur={onBlur}
id={id}
value={content}
></textarea>
)}
{error && touched && (
<div className={classes['error-message']}>{error}</div>
)}
</div>
);
};
The part of the following component most relevant to answering the question is the following:
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}

Why is the min state getting updated 2 times instead of only once?

Why is the min state getting updated in the multiples of two instead of just updating by one after every 59 seconds? How do I fix it?
import { useRef, useState } from "react";
export const Timer = () => {
const [second, setSecond] = useState(0);
const [min, setMin] = useState(0);
const watch = useRef(null);
const startTimer = () => {
watch.current = setInterval(() => {
setSecond((value) => {
if (value === 59) {
setSecond(0);
setMin((v) => v + 1);
}
return value + 1;
});
}, 1000);
};
return (
<div>
<h1>
{min}:{second}{" "}
</h1>
<button onClick={startTimer}>Start</button>
<button onClick={() => clearInterval(watch.current)}>Pause</button>
<button
onClick={() => {
setSecond(0);
return clearInterval(watch.current);
}}
>
Reset
</button>
</div>
);
};
This is the component as a whole. I am new to react so please help.

useState with an argument in it's array is breaking a setInterval and makes it glitchy and erratic

I just asked a question earlier here: react value of a state variable different in a different function
and now I have a new problem.
having a useEffect that looks like this
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
is breaking a setInterval that looks like this:
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
The full component looks like this:
import React, { useEffect, useState } from 'react';
function ScoreCard() {
const [strokeScore, setStrokeScore] = useState(1);
const [totalStrokeScore, setTotalStrokeScore] = useState(1);
const [strokeCountdown, setStrokeCountdown] = useState();
const strokeCountdownDing = new Audio('/sounds/round-complete.mp3');
// make new variable, maybe?
let strokeScoreCount = 0;
const endOfGameRound = () => {
strokeCountdownDing.play();
document.getElementById('stroke-counter-button').disabled = true;
}
const addToStrokeScore = () => {
setStrokeScore(prev => prev + 1);
// prints the correct number
console.log('Score in function', strokeScore);
if (strokeCountdown === 0) {
endOfGameRound()
}
}
const subtractStrokeScore = () => {
setStrokeScore(strokeScore - 1);
}
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
return (
<div className="game__score-card">
<div className="game__speed-level">
Speed: idk
</div>
<div className="game__stroke-countdown">
Countdown: {strokeCountdown}
</div>
<p>Score: {strokeScore}</p>
<button id="stroke-counter-button" onClick={addToStrokeScore}>
{strokeCountdown === 0 ? 'Game Over' : 'Stroke'}
</button>
{/* window.location just temp for now */}
{strokeCountdown === 0
? <button onClick={() => window.location.reload(false)}>Play Again</button>
: <button disabled>Game in Progress</button>
}
<div className="game__total-score">
Total score: {totalStrokeScore}
</div>
</div>
);
}
export default ScoreCard;
When I click on the button, the timer gets erratic and goes all over the place.
All I want to do is make it so that the timer counts down smoothly, gets the clicks the user made and add it to total score.
Why is
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
Breaking everything?
I was calling countDown() everytime I clicked so I just did
useEffect(() => {
if (strokeScore === 1) {
countDown();
}
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);

Reset countdown timer in on button click in react

i have this code basically a kind of quiz app. I want to do rest timer when user click on next or previous button. Need help for this. We can do that by creating only single component and create one of state as reset but in that case every second main page get re-render.
const CalcTimeLeft = () => {
const [time, setTime] = useState(300000);
useEffect(() => {
setTimeout(() => {
setTime(time - 1000);
}, 1000);
}, [time]);
const timeLeft = {
minutes: Math.floor(time / 60 / 1000) % 60,
seconds: Math.floor(time / 1000) % 60
};
return <div>{`${timeLeft.minutes}:${timeLeft.seconds}`}</div>;
};
export default function App() {
const [currPage, setCurrpage] = useState(1);
const question = [
];
const handleNext = () => {
setCurrpage(currPage + 1);
};
const handlePrev = () => {
setCurrpage(currPage - 1);
};
let item_per_page = 1;
let total_page = Math.floor(question.length / item_per_page);
const data = question.splice((currPage - 1) * item_per_page, item_per_page);
return (
<div className="App">
{data.map((data) => {
return <div>{data.question}</div>;
})}
<CalcTimeLeft />
{currPage !== total_page && <button onClick={handleNext}>Next</button>}
{currPage !== 1 && (
<button onClick={() => setCurrpage(handlePrev)}>Previous</button>
)}
</div>
);
}
You can bubble the reset event in the CalcTimeLeft component.
const CalcTimeLeft = () => {
const [time, setTime] = useState(300000);
const resetTime = () => {
setTime(300000);
};
useEffect(() => {
setTimeout(() => {
setTime(time - 1000);
}, 1000);
}, [time]);
return { resetTime };
};
export default function App() {
const [currPage, setCurrpage] = useState(1);
const question = [];
const { resetTime } = CalcTimeLeft();
const handleNext = () => {
resetTime;
setCurrpage(currPage + 1);
};
const handlePrev = () => {
resetTime;
setCurrpage(currPage - 1);
};
const timeLeft = {
minutes: Math.floor(time / 60 / 1000) % 60,
seconds: Math.floor(time / 1000) % 60,
};
let item_per_page = 1;
let total_page = Math.floor(question.length / item_per_page);
const data = question.splice((currPage - 1) * item_per_page, item_per_page);
return (
<div className="App">
{data.map((data) => {
return <div>{data.question}</div>;
})}
<div>{`${timeLeft.minutes}:${timeLeft.seconds}`}</div>
{currPage !== total_page && <button onClick={handleNext()}>Next</button>}
{currPage !== 1 && <button onClick={handlePrev()}>Previous</button>}
</div>
);
}

How to reset the timer using UseEffect React Hooks

I'm currently trying to make a pomodoro timer. One feature I noticed on some timers is that if you click reset time, it'll go to the default time (usually 15 minutes).
I want it to be a little more advanced so that when you click reset, it will reset to whatever value you set the timer to.
For example, when you open the screen the default time is 15 minutes. If you add 5 minutes, then press start, then press reset, it should reset to 20 minutes. Instead, my code is resetting it to 15.
Can anyone think of a way to reset time?
Apologies if it's a bad explanation. Thank you in advance.
Here's my code so far using React Hooks:
import './App.css';
function App() {
const [session, setSession] = useState(5)
const [timer, setTimer] = useState(2)
const [isRunning, setIsRunning] = useState(false)
const [resetTime, setResetTime] = useState(900)
let time = new Date(timer * 1000).toISOString().substr(11, 8);
function sessionIncrement() {
setSession(prevSession => session + 1)
}
function sessionDecrement() {
if (session > 0) {
setIsRunning(false)
setSession(prevSession => prevSession > 0 && prevSession - 1)
}
}
function resetTimer() {
setIsRunning(false)
setTimer(resetTime)
}
function increment() {
setIsRunning(false);
setTimer(prevTimer => prevTimer + 300);
}
function decrement() {
setIsRunning(false);
setTimer(prevTimer => prevTimer - 300);
}
useEffect(() => {
if (isRunning) {
const interval = setInterval(() => {
setTimer(prevTimer => prevTimer > 0 && prevTimer - 1)
}, 1000);
if (timer === 0) {
sessionDecrement()
setIsRunning(false)
}
return () => clearInterval(interval)
}
}, [isRunning, session, timer])
useEffect(() => {
setResetTime(timer)
}, [])
return (
<div className="App">
<h1>Session #{session}</h1>
<button onClick={() => sessionDecrement()}>-</button>
<button onClick={() => sessionIncrement()}>+</button>
<h1>{time}</h1>
<button onClick={() => setIsRunning(false)} >Pause</button>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => resetTimer()}>Reset</button>
<button onClick={() => decrement()}>-</button>
<button onClick={() => increment()}>+</button>
</div>
);
}
export default App;
The first thing you'll want to do is change the timer and reset time to use the same value:
const initialTime = 900;
const [session, setSession] = useState(5);
const [timer, setTimer] = useState(initialTime);
const [isRunning, setIsRunning] = useState(false);
const [resetTime, setResetTime] = useState(initialTime);
Then, you'll want to change increment and decrement functions so that they change the reset value as well as the timer value:
function increment() {
setIsRunning(false);
setTimer((prevTimer) => prevTimer + 300);
setResetTime((prevResetTime) => prevResetTime + 300);
}
function decrement() {
setIsRunning(false);
setTimer((prevTimer) => prevTimer - 300);
setResetTime((prevResetTime) => prevResetTime - 300);
}
If I understand you right, you just need to set resetTime to the same value as timer, in the increment and decrement function. Like this:
import './App.css';
function App() {
const [session, setSession] = useState(5)
const [timer, setTimer] = useState(2)
const [isRunning, setIsRunning] = useState(false)
const [resetTime, setResetTime] = useState(900)
let time = new Date(timer * 1000).toISOString().substr(11, 8);
function sessionIncrement() {
setSession(prevSession => session + 1)
}
function sessionDecrement() {
if (session > 0) {
setIsRunning(false)
setSession(prevSession => prevSession > 0 && prevSession - 1)
}
}
function resetTimer() {
setIsRunning(false)
setTimer(resetTime)
}
function increment() {
const newTime = timer + 300
setIsRunning(false);
setTimer(newTime);
setResetTime(newTime)
}
function decrement() {
const newTime = timer - 300
setIsRunning(false);
setTimer(newTime);
setResetTime(newTime)
}
useEffect(() => {
if (isRunning) {
const interval = setInterval(() => {
setTimer(prevTimer => prevTimer > 0 && prevTimer - 1)
}, 1000);
if (timer === 0) {
sessionDecrement()
setIsRunning(false)
}
return () => clearInterval(interval)
}
}, [isRunning, session, timer])
useEffect(() => {
setResetTime(timer)
}, [])
return (
<div className="App">
<h1>Session #{session}</h1>
<button onClick={() => sessionDecrement()}>-</button>
<button onClick={() => sessionIncrement()}>+</button>
<h1>{time}</h1>
<button onClick={() => setIsRunning(false)} >Pause</button>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => resetTimer()}>Reset</button>
<button onClick={() => decrement()}>-</button>
<button onClick={() => increment()}>+</button>
</div>
);
}
export default App;

Resources