Building a React clock with hooks - javascript

I'm working on building a clock that counts up, just a practice exercise (I know there are better ways of doing this).
My issue is when a minute is added, the "addMinute" seems to run twice, and I can't for the life of me figure out how or why.
Here is a demo on codesandbox: https://codesandbox.io/s/restless-frost-bud7p
And here is the code at a glance:
(please note, the counter only counts up to 3 and counts faster; this is just for testing purposes)
const Clock = (props) => {
const [seconds, setSeconds] = useState(0)
const [minutes, setMinutes] = useState(0)
const [hours, setHours] = useState(0)
const addHour = () => {
setHours(p => p + 1)
}
const addMinute = () => {
setMinutes(prev => {
if (prev === 3) {
addHour()
return 0
} else {
return prev + 1
}
})
}
const addSecond = () => {
setSeconds(prevState => {
if (prevState === 3) {
addMinute()
return 0
} else {
return prevState + 1
}
})
}
useEffect(() => {
const timer = setInterval(addSecond, 600)
return () => clearInterval(timer)
}, [])
return (
<div>
<h1>time!</h1>
<p>
{hours < 10 ? 0 : ""}{hours}
:
{minutes < 10 ? 0 : ""}{minutes}
:
{seconds < 10 ? 0 : ""}{seconds}
</p>
<p>
{seconds.toString()}
</p>
</div>
)
}

The issue is that you are using the React.StrictMode wrapper in the index.js file.
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
So you should decide between using strict mode or having side effects, the easy way is just removing the React.StrictMode wrapper. The other way is removing side effects, where you only need to do the following:
Update your addSecond and addMinute functions to something like:
const addMinute = () => {
setMinutes((prev) => prev + 1);
};
const addSecond = () => {
setSeconds((prevState) => prevState + 1);
};
And your useEffect call to something like:
useEffect(() => {
if(seconds === 3) {
addMinute();
setSeconds(0);
};
if(minutes === 3) {
addHour();
setMinutes(0);
}
const timer = setInterval(addSecond, 600);
return () => clearInterval(timer);
}, [seconds, minutes]);
Here an updated version of your code: https://codesandbox.io/s/goofy-lake-1i9xf

A couple of issues,
first you need to use prev for minutes, so
const addMinute = () => {
setMinutes(prev => {
if (prev === 3) {
addHour()
return 0
} else {
return prev + 1
}
})
}
And then you need to remove the React.StrictMode wrapper component from index, which is what is actually causing the double increase, as part of what the strict mode does is
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
see: https://codesandbox.io/s/pensive-wildflower-pujmk

So I had no idea about strict mode and the intentional double renders. After reading the documentation I finally understand the purpose of this.
As such, it appears the best solution is to have no side effects from the useEffect, and instead, handle that logic outside of the effect, but still changing every second.
So, I set an effect that has a piece of state starting at zero and going up by one per second.
Then, with each change of "time", the useMemo will recalculate how many hours, mins and seconds the total time is.
The only thing I don't like is all those calculations running every render! (But realistically those take but a few miliseconds, so performance doesn't seem to be an issue).
const [time, setTime] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setTime(p => p + 1)
}, 999);
return () => clearTimeout(timer);
}, [userState]);
const timeJSON = useMemo(() => {
const hrs = Math.floor(time/3600)
const mins = Math.floor( (time-(3600*hrs)) / 60 )
const secs = time - (3600 * hrs) - (60*mins)
return {
hrs,
mins,
secs,
}
}, [time])
return (
<div>
<p>
{timeJSON.hrs < 10 ? 0 : ""}{timeJSON.hrs}
:
{timeJSON.mins < 10 ? 0 : ""}{timeJSON.mins}
:
{timeJSON.secs < 10 ? 0 : ""}{timeJSON.secs}
</p>
</div>
)
Thanks again for everyone pointing me in the right direction on this!

Related

Javascript alternate player turn

I am currently working on a board game like chess.
I can't seem to make alternate turn work.
const clickPiece = (e: React.MouseEvent) => {
const element = e.target as HTMLElement;
const shogiBoard = shogiBoardRef.current;
let steps = 1;
if (steps % 2 === 1) {
setActivePlayer(Player.US);
steps++;
} else if (steps % 2 === 0) {
setActivePlayer(Player.ENEMY);
steps++;
}
if (element.classList.contains("shogiPiece") && shogiBoard && activePlayer) {
const takeX = Math.floor((e.clientX - shogiBoard.offsetLeft) / CELL_SIZE);
const takeY = Math.abs(Math.ceil((e.clientY - shogiBoard.offsetTop - BOARD_SIZE) / CELL_SIZE));
setTakePosition({
x: takeX,
y: takeY,
});
const x = e.clientX - CELL_SIZE / 2;
const y = e.clientY - CELL_SIZE / 2;
element.style.position = "absolute";
element.style.left = `${x}px`;
element.style.top = `${y}px`;
setActivePiece(element);
}
};
activePlayer initially Player.US which comes from an enum:
export enum Player {
ENEMY,
US,
}
activePlayer useState:
const [activePlayer, setActivePlayer] = useState<Player>(Player.US);
For me to alternate turns seemed the easiest when grabbing a piece, check which player is up then change, tried it with increasing the number of steps and check for remainder but it is not working.
Thank you in advance for the suggestions and help.
The root cause your code is not working is how you are using step.
Step is part of your application's state, so this should be an state, not a simple variable. Your are actually resetting step = 1 in each render
First define [step, setStep] using useState:
const [step, setStep] = useState<number>(1); // instead of let steps = 1
and then update the step like this:
setStep(i => i+1) // instead of steps++
You can use this function to toggle the active player: check which is the current player and then set the other one:
TS Playground
import {useCallback, useState} from 'react';
enum Player { ENEMY, US }
function Component () {
const [activePlayer, setActivePlayer] = useState<Player>(Player.US);
const setAlternatePlayer = useCallback(
() => setActivePlayer(player => player === Player.US ? Player.ENEMY : Player.US),
[],
);
// Use function "setAlternatePlayer"...
}
This is because you if condition is wrong !
Change your if statement to this:
if (steps % 2 === 0) {
setActivePlayer(Player.ENEMY);
steps++;
} else {
setActivePlayer(Player.US);
steps++;
}
This will work

Create a function that returns the next/prev 4 results in an array of objects

I'm trying to create a function that renders the next & prev 4 results in an array of objects onClick. Currently I am only returning the first 4 items and their images and adjusting the values in the return statement onClick and am not happy with this solution. Any pointers?
const ImageSlider = ({ loadData, data, setData }) => {
const [current, setCurrent] = useState(0);
const length = data.length;
const [test, setTest] = useState(0);
const [another, setAnother] = useState(4);
useEffect(() => {
loadData().then((data) => setData(data));
}, []);
const getNextSlide = () => {
setCurrent(current === length - 1 ? 0 : current + 1);
if (current / 3 === 1) {
setTest(test + 4);
setAnother(another + 4);
}
};
const getPrevSlide = () => {
setCurrent(current === 0 ? length - 1 : current - 1);
// setTest(test - 1);
// setAnother(another - 1);
};
console.log(current);
if (!Array.isArray(data) || length <= 0) {
return null;
// Need that error message here
}
return (
<div className="slider">
<FaArrowCircleLeft className="slider-left-arrow" onClick={getPrevSlide} />
<FaArrowCircleRight
className="slider-right-arrow"
onClick={getNextSlide}
/>
{data.slice(test, another).map((program, index) => {
return (
<div
className={
index === current
? "slider-active-program"
: "slider-inactive-program"
}
key={index}
>
<img
src={program.image}
alt="program"
className="slider-program-image"
/>
</div>
);
})}
</div>
);
};
export default ImageSlider;
This may be one possible approach to achieve the desired objective:
const ImageSlider = ({ loadData, data, setData }) => {
const imageLength = 4; // number of images to be shown per-page
const totalImages = data.length;
const [startAt, setStartAt] = useSate(0); // start rendering from image-0
useEffect(() => {
loadData().then((data) => setData(data));
}, []);
const switchImages = (direction = 'forward') => (
setStartAt(prev => {
if (direction === 'forward') {
// if there are more images, reposition 'startAt' to next set
if (prev + imageLength < totalImages) return prev + imageLength;
return 0; // reset back to image-0
} else {
// if there is previous set, reposition to it's start
if (prev - imageLength >= 0) return prev - imageLength;
// otherwise, reposition to the start of the last-set
return (Math.floor((totalImages - 1) / imageLength) * imageLength)
};
})
);
return (
<div className="slider">
<FaArrowCircleLeft
className="slider-left-arrow"
onClick={() => switchImages('reverse')}
/>
<FaArrowCircleRight
className="slider-right-arrow"
onClick={() => switchImages()}
/>
{Array.isArray(data) && data.filter((el, idx) => idx >= startAt && idx < (startAt + imageLength)).map((program, index) => {
return (
<div
className={
index === current
? "slider-active-program"
: "slider-inactive-program"
}
key={index}
>
<img
src={program.image}
alt="program"
className="slider-program-image"
/>
</div>
);
})}
</div>
);
};
export default ImageSlider;
Explanation
imageLength is fixed (set to 4)
startAt is a state-variable (initially set to 0)
data is rendered on return only when it is an Array
switchImages is a method invoked on both left-arrow and right-arrow click-events (changing the parameter direction to reverse on left-arrow)
startAt is updated based on the direction, the prev value, the totalImages and imageLength.
Suggestion
If one needs to render a message to the user that the data array is either not-yet-loaded or it is empty, it may be acccomplished by changing the {Array.isArray(data) && .... into a conditional such as {Array.isArray(data) ? ....<existing> : <some message here>.
Warning
The above code is only an approach. It is not tested & may contain syntax errors or other issues. Please use comments to share feedback and this answer may be updated accordingly.
I don't think you have to change much about your approach to get what you're looking for here.
To render the images to the left and right of your current selection and allow looping from the front to the back (as it looks like you want to do), we can paste a copy of the array to the front and back and select the middle slice. It's a little bit like this:
The implementation would just require you to replace the current .map(...) method in your return statement with this one:
[...data, ...data, ...data].slice(
length + current - peekLeft,
length + current + peekRight + 1
).map((program, index) => {
...
}
And then you can also remove the test and another state objects as well since they aren't necessary. You just need to add the peekLeft and peekRight or replace them inline inside the .map(...) statement. The first option may look something like this:
const peekLeft = 4;
const peekRight = 4;
const ImageSlider = ({ loadData, data, setData }) => {
...
}

Timer slows down when performing other tasks in React

This might be a very common or easy issue to solve, but I'm very new to React (started today) and am not aware of this.
I'm trying to build a simple clicks per second application. Basically, an application that counts how many clicks you do per second. So, when the person starts clicking on the button a timer starts, and at the end, I divide clicks/seconds. The problem is that the timer slows down when I start clicking faster.
Here is a simplified version of my code:
function Clicker() {
const [clicks, setClicks] = useState(0);
const [milliSeconds, setMilliSeconds] = useState(0);
const [storeMilliSeconds, setStoreMilliSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [startTimer, setStartTimer] = useState(false);
const clickerHandler = () => {
setEffect(true);
setStartTimer(true);
setClicks(clicks + 1);
};
useEffect(() => {
if (seconds === 10 && milliSeconds === 0) {
setStartTimer(false);
}
if (startTimer === true) {
const interval = setInterval(() => {
setMilliSeconds((milliSeconds) => milliSeconds + 1);
setStoreMilliSeconds((storeMilliSeconds) => storeMilliSeconds + 1);
if (milliSeconds === 9) {
setSeconds((seconds) => seconds + 1);
setMilliSeconds(0);
}
}, 100);
return () => clearInterval(interval);
}
return console.log(
"nothing happening"
);
});
return (
<button
onClick={clickerHandler}
>
There's only one way to find out...
</button>
I'm very grateful for the help I can get, thanks!

Is there a way to iterate through a timer in React?

I'm trying to create a Pomodoro timer using Hooks and I have set up the basic functionality using useState and useEffect. I have a 25-minute timer that counts down and every time it gets to 0, it starts a break timer of 5 minutes. What I'm trying to figure out now is how to create an iteration that says "every 4 times the timer hits 0, change the break time from 5 minutes to 15 minutes and then, go back to 5 minutes." I thought of creating sessions that way it will say 4th session and then it will go back to 1. but I'm really not sure what to do here.
import React, { useState, useEffect } from "react";
function Pomodoro() {
const [minutes, setMinutes] = useState(25);
const [seconds, setSeconds] = useState(0);
const [displayMessage, setDisplayMessage] = useState(false);
const [session, setSession] = useState(1);
useEffect(() => {
let interval = setInterval(() => {
clearInterval(interval);
if (seconds === 0 && minutes !== 0) {
setSeconds(59);
setMinutes(minutes -1);
} else if (seconds === 0 && minutes === 0) {
let minutes = displayMessage ? 24 : 4;
let seconds = 59;
setSeconds(seconds);
setMinutes(minutes);
setDisplayMessage(!displayMessage);
} else {
setSeconds(seconds -1);
}
}, 1000);
}, [seconds]);
const timerMinutes = minutes < 10 ? `0${minutes}` : minutes;
const timerSeconds = seconds < 10 ? `0${seconds}` : seconds;
return (
<div className="pomodoro">
<div>Session:{session} </div>
<div className="message">
{displayMessage && <div>Break time! New Session starts in:</div>}
</div>
<div className="timer">
{timerMinutes}:{timerSeconds}
</div>
</div>
);
}
export default Pomodoro;
Your approach using a counter to keep track of the completed sessions seems to make sense. If you want to use a different amount of break time for every fourth iteration, you could use the remainder operator as below:
let breakTime = (session % 4) === 0 ? 14 : 0;
Then, you just need to make sure you are incrementing your session variable by one each time you complete a session. This also means you only want to increase it when you are not "on break" so you must make sure to guard against that.
Updating the answer with the full code that I tested to be working. Note the following changes I made:
I am only keeping track of the timer in seconds - this reduces the complexity inside useEffect and you can convert from seconds to other formats (try using the remainder operator again)
Moved the period lengths to constants
Renamed the variable displayMessage to isOnBreak for clarity
import React, { useState, useEffect } from "react";
// Define the period lengths (in seconds)
const workTime = 2;
const shortBreakTime = 4;
const longBreakTime = 6;
function Pomodoro() {
const [seconds, setSeconds] = useState(workTime);
// Renamed this variable for clarity to indicate it is a boolean
const [isOnBreak, setIsOnBreak] = useState(false);
const [session, setSession] = useState(1);
useEffect(() => {
let interval = setInterval(() => {
clearInterval(interval);
if (seconds === 0) {
let breakTime = (session % 4 === 0) ? longBreakTime : shortBreakTime;
let seconds = !isOnBreak ? breakTime : workTime;
// A session is complete when work and break is done,
// so only increment when finishing a break
if (isOnBreak) setSession(session+1);
setSeconds(seconds);
setIsOnBreak(!isOnBreak);
} else {
setSeconds(seconds -1);
}
}, 1000);
}, [seconds]);
// Here you could convert from seconds to minutes and seconds or whatever time format you prefer
const timerSeconds = seconds < 10 ? `0${seconds}` : seconds;
return (
<div className="pomodoro">
<div>Session:{session} </div>
<div className="message">
{isOnBreak && <div>Break time! New Session starts in:</div>}
</div>
<div className="timer">
{timerSeconds}
</div>
</div>
);
}

Why setInterval does not work correactly?

I am trying to make a stopwatch (00:00:00:00).But my one sec is slower than real one sec.
I also changed the value of setInterval 10 to 1 but nothing changed. When I changed it as 100, it worked, time flowed slower.
(00:00:00:00)=(hh:mm:ss:ms)
Here is a part of my code:
const [time, setTime] = useState({
ms: 0,
ss: 0,
mm: 0,
hh: 0
})
let degisenMs = time.ms,
degisenH = time.hh,
degisenM = time.mm,
degisenS = time.ss;
const run = () => {
if (updatedMs === 100) {
updatedS++;
updatedMs = 0
}
if (updatedS === 60) {
updatedM++;
updatedS = 0;
}
if (M === 60) {
updatedH++;
updatedM = 0
}
updatedMs++;
return (setTime({
ms: updatedMs,
ss: updatedS,
mm: updatedM,
hh: updatedH
}))
}
const start = () => {
setStatus(1)
run()
setInterv(setInterval(run, 10))
}
The problem is that setInterval is not exact, it is approximate. One option is to use web workers to increase accuracy as described in the link, but it is still not exact.
When it comes to measuring time, it is better to track the start timestamp and figure out how much time has passed at each tick/update. You can then update the UI or trigger an alarm etc. Here is some pseudocode.
const [ startTime, setStartTime ] = useState(null)
const [ intervalId, setIntervalId ] = useState(null)
function tick() {
const now = new Date()
const elapsedMs = now - startTime
// Update UI etc using elapsedMs
}
function start() {
setStartTime(new Date())
// Run tick() every 100ms
setIntervalId(setInterval(tick, 100))
}
function stop() {
clearInterval(intervalId)
}

Categories