I am trying to create a simple counter component in React. It should start at 0, then iterate up to the passed in number. Here is the code I currently have:
const Counter = ({ number }) => {
const [currentNumber, setCurrentNumber] = useState(0);
for (let i = 0; i < number; i++) {
setTimeout(() => setCurrentNumber(currentNumber + 1), 2000);
}
return <span>{currentNumber}</span>;
};
export default Counter;
What happens when I run this, is it basically keeps counting forever. currentNumber never stops incremementing, even once the number has been reached.
You should add currentNumber as a dependency for useEffect(). This way useEffect() will get triggered every second and a new timeout will be registered but only as long as currentNumber is smaller than number.
const Counter = ({ number }) => {
const [currentNumber, setCurrentNumber] = React.useState(0);
React.useEffect(() => {
if(currentNumber < number) setTimeout(() => setCurrentNumber(currentNumber + 1), 1000);
}, [currentNumber]);
return <span>{currentNumber}</span>;
};
ReactDOM.render(<Counter number={20}/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I ran the code and it's getting stuck. Reason:
You're scheduling 5 timers which will all update currentNumber to currentNumber + 1. currentNumber is 0, so it will update number times to 1 after 2 seconds. You can approach this with an interval or a useEffect:
useEffect(() => {
if (currentNumber < number)
setTimeout(() => setCurrentNumber(n => n + 1), 2000)
}, [number, currentNumber])
If you're using an interval, make sure you return a cleanup callback from useEffect, like that:
useEffect(() => {
const id = useInterval(...)
return () => clearInterval(id)
}, [...])
Please try this code:
const [currentNumber, setCurrentNumber] = useState(0);
useEffect(() => {
if (currentNumber >= number) return;
setTimeout(() => {
setCurrentNumber(currentNumber + 1);
}, 1000);
}, [currentNumber]);
return <span>{currentNumber}</span>;
Related
I want to make a countdown timer with start, stop, resume and reset buttons. However, I could not figure out why my code does not work. I am guessing that the issue lies in the part on timer, setInterval and clearInterval.
I have attached snippets of my code below.
Help would be greatly appreciated, thanks!
function Countdown() {
const [ timerRunning, setTimerRunning ] = useState(false);
const [ startTime, setStartTime ] = useState(0);
const [ totalTime, setTotalTime ] = useState(0);
var timer;
const startTimer = () => {
setTimerRunning(true);
setStartTime(totalTime);
setTotalTime(totalTime);
timer = setInterval(() => {
const remainingTime = totalTime - 1000;
if (remainingTime >= 0) {
setTotalTime(remainingTime);
} else {
clearInterval(timer);
setTimerRunning(false);
setStartTime(0);
setTotalTime(0);
}
}, 1000);
};
const stopTimer = () => {
clearInterval(timer);
setTimerRunning(false);
};
you have to put your setInterval in React.useEffect take a look at this post
some suggestions for your code:
use className instead of class attribute in your elements.
don't use var or let in react functional components.
useState doesn't change value immediately. So it's value is not actually changing. To avoid this use UseEffect hook.
useEffect(() => {
let intervalid
if (timerRunning){
intervalid = setInterval(() => {
const remainingTime = totalTime - 1000;
if (remainingTime >= 0) {
setTotalTime(remainingTime);
} else {
clearInterval(intervalid);
setTimerRunning(false);
setStartTime(0);
setTotalTime(0);
}
}, 1000);
}
return () => {
clearInterval(intervalid)
}
}, [timerRunning,totalTime])
My goal is to loop through an array of characters and end on each letter of a given word. My code is currently displaying all of these elements at once, but I want them to display sequentially. Here's what I currently have:
Current view
I'd like to return the array that ends with h(wait a few moments), array that ends with e (wait a few moments), and so on. I can't figure out to to attach the arrayIndex to the nested map though.
DisplayName.js
import React, { useState, useEffect } from "react";
const DisplayName = ({ characters, first }) => {
const [charIndex, setCharIndex] = useState(0);
const [arrayIndex, setArrayIndex] = useState(0);
let arrayContainer = [];
first.map((letter, i) => {
arrayContainer.push([]);
arrayContainer[i].push(characters.concat(first[i]));
return arrayContainer;
});
// I can't figure out how to attach arrayIndex here. I am
// also not using j currently, but kept it for now in case I need
// a key for the return statements.
const fullList = arrayContainer.map((letterArr, j) => {
return letterArr.map(char => {
return (char[charIndex])
})
});
useEffect(() => {
let timer;
let secondTimer;
if (charIndex < characters.length) {
timer = setTimeout(() => {
setCharIndex(charIndex + 1)
}, 75)
}
if (arrayIndex < first.length - 1) {
secondTimer = setTimeout(() => {
setArrayIndex(arrayIndex + 1)
}, 75)
}
return () => {
clearTimeout(timer);
clearTimeout(secondTimer);
};
}, [charIndex, characters, arrayIndex, first]);
return (
<div>{fullList}</div>
)
};
export default DisplayName;
App.js
import React from 'react';
import DisplayName from './DisplayName';
import './App.css';
function App() {
const first = 'hello'.split('');
const funChars = [
'⏀', '⎷', '⌮', '⋙', '⊠', '⎳', '⍼',
'⍣', '╈', '╳', '☀', '★', '☍', 'ↂ','▅'];
return (
<div className="glow" style={{ minHeight: '100vh'}}>
<span style={{ letterSpacing: 12}}><DisplayName first={first} characters={funChars}/></span>
</div>
);
}
export default App;
I've also tried something like const [rendered, setRendered] = useState(false); without success, which I tried attaching to the j key.
If I understand your question, you want to iterate over the first string up to an index and display a "rolling" fun character while iterating the string.
Intuitively I think it is easier to think of of slicing the front of the first string to an index, and appending the fun character.
iteration
index
text.substring(0, index)
result(s)
0
0
""
'⏀', '⎷', '⌮',...
1
1
"h"
'h⏀', 'h⎷', 'h⌮',...
2
2
"he"
'he⏀', 'he⎷', 'he⌮',...
3
3
"hel"
'hel⏀', 'hel⎷', 'hel⌮',...
4
4
"hell"
'hell⏀', 'hell⎷', 'hell⌮',...
5
5
"hello"
'hello'
The tricky issue is using two separate timers/intervals to increment the index for the first string and to increment an index into the fun characters array. Here is a solution I came up with.
Use a React ref to hold a interval timer reference for the rolling fun characters.
Single useEffect hook to start the "rolling" fun character index incrementing on an interval. Start a timeout on incrementing over the first string char array, if there is still length to iterate, enqueue another timeout, otherwise run clean up functions to clear timers and state.
Slice the first string up to index arrayIndex and conditionally append a "rolling" fun character.
Code:
const DisplayName = ({ characters, first }) => {
const charTimerRef = useRef(null);
const [charIndex, setCharIndex] = useState(null);
const [arrayIndex, setArrayIndex] = useState(0);
useEffect(() => {
let timerId;
const cleanupTimerRef = () => {
setCharIndex(null);
clearInterval(charTimerRef.current);
charTimerRef.current = null;
};
if (!charTimerRef.current) {
setCharIndex(0);
charTimerRef.current = setInterval(() => {
setCharIndex((i) => i + 1);
}, 75);
}
if (arrayIndex < first.length) {
timerId = setTimeout(() => {
setArrayIndex((i) => i + 1);
}, 1000);
} else {
cleanupTimerRef();
}
return () => {
clearTimeout(timerId);
cleanupTimerRef();
};
}, [arrayIndex, first]);
const fullList =
first.substring(0, arrayIndex) +
(charIndex ? characters[charIndex % characters.length] : "");
return <div>{fullList}</div>;
};
Demo
I'm trying to build a pomodoro timer with a pause option. There's an analogue clock and a digital timer. My issue is with the digital timer - I can pause it by clearing the interval but do not know how to resume it without starting from the top (with a new setInterval).
This is the codesandbox of the project.
This is the relevant part from the DigitalClock component:
const timer = () => {
const now = Date.now()
const then = now + mode.duration * 60 * 1000
countdown = setInterval(() => { // That's how I resume it (with a re-render)
const secondsLeft = Math.round((then - Date.now()) / 1000)
if(secondsLeft < 0 || pause) {
clearInterval(countdown) // That's how I pause it (by clearing the interval)
return;
}
displayTimeLeft(secondsLeft)
}, 1000)
}
I think pausing the timer could be done with a boolean instead of clearing the interval,
So let's say that u have also a boolean keeping track of if it's paused on top level
let paused = false;
and you should consider looking up for if timer is not paused then do the math inside so
countdown = setInterval(() => { // That's how I resume it (with a re-render)
if(!paused) {
const secondsLeft = Math.round((then - Date.now()) / 1000)
if(secondsLeft < 0 || pause) {
clearInterval(countdown) // That's how I pause it (by clearing the interval)
return;
}
displayTimeLeft(secondsLeft)
}
}, 1000)
The only thing that's left is to toggle this paused boolean to true/false when someone click's the Pause button.
I don't know about React that much but that would be the choice I would go if i was doing this task :)
Possible solution - don't clear the interval when it is paused, just don't update the secondsLeft on the tick
Also, secondsLeft can be an integer, it doesn't have to be related to the actual time.
// global variables
var pause = false;
var elapsed, secondsLeft = 60;
const timer = () => {
// setInterval for every second
countdown = setInterval(() => {
// if allowed time is used up, clear interval
if (secondsLeft < 0) {
clearInterval(countdown)
return;
}
// if paused, record elapsed time and return
if (pause === true) {
elapsed = secondsLeft;
return;
}
// decrement seconds left
secondsLeft--;
displayTimeLeft(secondsLeft)
}, 1000)
}
timer();
const displayTimeLeft = (seconds) => {
document.getElementById("time").textContent = seconds;
}
document.getElementById("pause").addEventListener("click", (evt) => {
pause = !pause;
evt.target.textContent = pause ? "resume" : "pause";
if (pause === false) {
secondsLeft = elapsed;
}
});
<div id="time"></div>
<button id="pause">pause</button>
Using react, the timer should be in a useEffect hook (assuming you're using functional components). The useInterval will be placed inside and will run naturally within, updating the display through the useEffect hook. Place the clear interval in the useEffect return statement so when the timer expires, the interval will clear itself.
Then using pause as a state variable, manage your timer with buttons.
const [seconds, setSeconds] = useState(30);
const [pause, setPause] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if(!pause) { //I used '!paused' because I set pause initially to false.
if (seconds > 0) {
setSeconds(seconds - 1);
}
}
}, 1000);
return () => clearInterval(interval);
});
const handlePauseToggle = () => {
setPause(!pause);
}
Add a button to click, and your pause feature is set up.
*** Side note, feel free to ignore***
It looks like you have a way to display the time already, but I think it would be easier if you use the 'seconds' state variable in curly brackets to display your timer instead of creating a function (see below).
<div>
<p>0:{seconds >= 10 ? {seconds} : `0${seconds}`}</p>
</div>
This will display the timer correctly in a single line. Minutes is easy to add and so on.
Just a thought to make your code easier.
Using custom hook
Follow the following steps:
Create a new state variable to store the counter value on resume
On start check if you have a resume value then use it otherwise use the initial counter
Here's the full custom hook:
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
startCounter(resume - 1);
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};
And here's an example of using it on codepen:
React useCountdown
This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 2 years ago.
function func(){
const [time, setTime] = useState(10);
var timeRemaining = 10;
const myInterval = setInterval(() => {
if (timeRemaining > 0) {
timeRemaining = timeRemaining - 1;
setTime(timeRemaining);
} else { clearInterval(myInterval) }
}, 1000);
return <div>{time}</div>
}
The code above works thanks to the variable timeRemaining. However, it stops working if I remove that variable (in order to keep the code clean):
const myInterval = setInterval(() => {
if (time> 0) { setTime(time-1); }
else { clearInterval(myInterval); }
}, 1000);
By rewriting it in the above way, it stops updating time.
Use effects to control interval, ref to hold reference to interval timer reference, and functional state update to correctly manage state.
Effect 1 - setup (mount) and cleanup (unmount) of interval effect
Effect 2 - clears interval when time reaches 0
Functional Component Code:
function App() {
const timerRef = useRef(null);
const [time, setTime] = useState(10);
useEffect(() => {
// Use pure functional state update to correctly queue up state updates
// from previous state time value.
// Store returned interval ref.
timerRef.current = setInterval(() => setTime(t => t - 1), 1000);
// Return effect cleanup function
return () => clearInterval(timerRef.current);
}, []); // <-- Empty dependency array, effect runs once on mount.
useEffect(() => {
// Clear interval and nullify timer ref when time reaches 0
if (time === 0) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, [time]); // <-- Effect runs on mount and when time value updates.
return <div>{time}</div>;
}
Use the function version of setTime, though I'd suggest putting your interval into a useEffect for cleanup as well
setTime((time) => {
if (time > 0) {
return time - 1;
}
clearInterval(myInterval);
return time;
});
With a useEffect for cleanup of the interval on unmount:
const {useState,useEffect} = React;
function Func() {
const [time, setTime] = useState(10);
useEffect(() => {
const myInterval = setInterval(() => {
setTime((time) => {
if (time > 0) {
return time - 1;
}
clearInterval(myInterval);
return time;
});
}, 1000);
return () => {
clearInterval(myInterval);
};
}, []);
return <div>{time}</div>;
}
ReactDOM.render(<Func />,document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
The problem is that you're creating a new setInterval every time the component was recreated, as a consequence of setTime execution.
As an alternative to the other answers, that put the setInterval calling inside an useEffect, you can also switch to a setTimeout and not using the useEffect:
const App = (props) => {
const [time, setTime] = useState(10);
setTimeout(() => {
if (time > 0) {
setTime(time - 1);
}
}, 1000);
return <div>{time}</div>;
};
Every time the component is redrawn as an effect of setTime call, the setTimeout will be fired again.
function func(){
const [time, setTime] = useState(10);
var timeRemaining = 10;
const myInterval = setInterval(() => {
if (timeRemaining > 0) {
timeRemaining = timeRemaining - 1;
setTime(timeRemaining);
} else { clearInterval(myInterval) }
}, 1000);
return <div>{time}</div>
}
Here with every render, JS would start a setInterval. And in setInterval's callback, variable timeRemaining allocated for every render in respect get stored, or we call it CLOSURE. So 1 sec after the very beginning, REACT render func again. And thus you get another interval and a variable timeRemaining. It'll run from timeRemaining = 10.
setTime will always be one thing. And time will be read from HOOKS's pools.
This code is WRONG! You'll have more and more interval with its timeRemaining.
Im trying to render a count down timer on screen with react hooks, but I'm not sure the best way to render it.
I know i'm supposed to use the useEffect to compare current state to previous state, but I don't think I'm doing it correctly.
I would appreciate the help!
I've tried a couple of different ways, none of them work, like setting state whenever whenever it updates, but it just ends up flickering like crazy.
const Timer = ({ seconds }) => {
const [timeLeft, setTimeLeft] = useState('');
const now = Date.now();
const then = now + seconds * 1000;
const countDown = setInterval(() => {
const secondsLeft = Math.round((then - Date.now()) / 1000);
if(secondsLeft <= 0) {
clearInterval(countDown);
console.log('done!');
return;
}
displayTimeLeft(secondsLeft);
}, 1000);
const displayTimeLeft = seconds => {
let minutesLeft = Math.floor(seconds/60) ;
let secondsLeft = seconds % 60;
minutesLeft = minutesLeft.toString().length === 1 ? "0" + minutesLeft : minutesLeft;
secondsLeft = secondsLeft.toString().length === 1 ? "0" + secondsLeft : secondsLeft;
return `${minutesLeft}:${secondsLeft}`;
}
useEffect(() => {
setInterval(() => {
setTimeLeft(displayTimeLeft(seconds));
}, 1000);
}, [seconds])
return (
<div><h1>{timeLeft}</h1></div>
)
}
export default Timer;```
const Timer = ({ seconds }) => {
// initialize timeLeft with the seconds prop
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
// exit early when we reach 0
if (!timeLeft) return;
// save intervalId to clear the interval when the
// component re-renders
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
// clear interval on re-render to avoid memory leaks
return () => clearInterval(intervalId);
// add timeLeft as a dependency to re-rerun the effect
// when we update it
}, [timeLeft]);
return (
<div>
<h1>{timeLeft}</h1>
</div>
);
};
You should use setInterval. I just wanted to add a slight improvement over #Asaf solution. You do not have to reset the interval every time you change the value. It's gonna remove the interval and add a new one every time (Might as well use a setTimeout in that case). So you can remove the dependencies of your useEffect (i.e. []):
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
const intervalId = setInterval(() => {
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>{timeLeft}s</div>;
}
Working example:
Note in the setter, we need to use this syntax (t) => t - 1 so that we get the latest value each time (see: https://reactjs.org/docs/hooks-reference.html#functional-updates).
Edit (22/10/2021)
If you want to use a setInterval and stop the counter at 0, here is what you can do:
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
const intervalRef = useRef(); // Add a ref to store the interval id
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// Add a listener to `timeLeft`
useEffect(() => {
if (timeLeft <= 0) {
clearInterval(intervalRef.current);
}
}, [timeLeft]);
return <div>{timeLeft}s</div>;
}
Here's another alternative with setTimeout
const useCountDown = (start) => {
const [counter, setCounter] = useState(start);
useEffect(() => {
if (counter === 0) {
return;
}
setTimeout(() => {
setCounter(counter - 1);
}, 1000);
}, [counter]);
return counter;
};
Example
Here's my version of a hook, with a "stop" countdown.
Also, I added a "fps" (frames p/sec), to show the countdown with decimal places!
import { useEffect, useRef, useState } from 'react'
interface ITimer {
timer: number
startTimer: (time: number) => void
stopTimer: () => void
}
interface IProps {
start?: number
fps?: number
}
const useCountDown = ({ start, fps }: IProps): ITimer => {
const [timer, setTimer] = useState(start || 0)
const intervalRef = useRef<NodeJS.Timer>()
const stopTimer = () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
const startTimer = (time: number) => {
setTimer(time)
}
useEffect(() => {
if (timer <= 0) return stopTimer()
intervalRef.current = setInterval(() => {
setTimer((t) => t - 1 / (fps || 1))
}, 1000 / (fps || 1))
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
}, [timer])
return { timer, startTimer, stopTimer }
}
export default useCountDown
Here is a small component - CountdownTimer - accepting an input parameter expiresIn representing the time left in seconds.
We use useState to define min and sec which we display on the screen, and also we use timeLeft to keep track of the time that's left.
We use useEffect to decrement timeLeft and recalculate min and sec every second.
Also, we use formatTime to format the minutes and seconds before displaying them on the screen. If minutes and seconds are both equal to zero we stop the countdown timer.
import { useState, useEffect } from 'react';
const CountdownTimer = ({expiresIn}) => {
const [min, setMin] = useState(0);
const [sec, setSec] = useState(0);
const [timeLeft, setTimeLeft] = useState(expiresIn);
const formatTime = (t) => t < 10 ? '0' + t : t;
useEffect(() => {
const interval = setInterval(() => {
const m = Math.floor(timeLeft / 60);
const s = timeLeft - m * 60;
setMin(m);
setSec(s);
if (m <= 0 && s <= 0) return () => clearInterval(interval);
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(interval);
}, [timeLeft]);
return (
<>
<span>{formatTime(min)}</span> : <span>{formatTime(sec)}</span>
</>
);
}
export default CountdownTimer;
Optionally we can pass a setter setIsTerminated to trigger an event in the parent component once the countdown is completed.
const CountdownTimer = ({expiresIn, setIsTerminated = null}) => {
...
For example, we can trigger it when minutes and seconds are both equal to zero:
if (m <= 0 && s <= 0) {
if (setTerminated) setIsTerminated(true);
return () => clearInterval(interval);
}