React: setting setInterval conditionally - reactjs

My timer never stops after reaching zero (it keeps counting down on screen). I did a console.log on timer and I see that, while the time on screen is counting down, console.log returns that timer never actually changes its value. Any ideas why? Am I not setting state properly or something like that?
class Clock extends Component {
constructor(props) {
super(props);
this.state = {break:5,
session:25,
timer: 1500}
this.handleClick = this.handleClick.bind(this);
this.handleTimer=this.handleTimer.bind(this);
}
handleClick(event){
const id= event.target.id;
let breakvar= this.state.break;
let sessionvar= this.state.session;
if (id==="break-increment" && breakvar<=59){
this.setState((state) => ({
break: this.state.break +1}));}
else if (id==="break-decrement" && breakvar>1){
this.setState((state) => ({
break: this.state.break -1}));}
else if(id==="reset"){
this.setState((state) => ({
break: 5, session: 25, timer: 1500}));
}
else if (id==="session-increment" && sessionvar <=59){
this.setState((state) => ({
session: this.state.session +1, timer: this.state.timer + 60}));}
else if(id==="session-decrement" && sessionvar>1){
this.setState((state) => ({
session: this.state.session -1, timer:this.state.timer - 60}));}
}
handleTimer(evt){
let timer=this.state.timer;
let Interval=setInterval(() => {
this.setState({
timer: this.state.timer - 1
})
console.log(timer) },1000)
if(timer ===0){
clearInterval(Interval)
}
}
Clock(){
let minutes = Math.floor(this.state.timer / 60);
let seconds = this.state.timer - minutes * 60;
seconds = seconds < 10 ? '0' + seconds : seconds;
minutes = minutes < 10 ? '0' + minutes : minutes;
return minutes + ':' + seconds;
}
render() {
return(
<div id="container">
<Display break={this.state.break} displayTime={this.Clock()} session={this.state.session}/>
<p id="break-label">Break length</p>
<Button onClick={this.handleClick} id="break-increment"/>
<Button onClick={this.handleClick} id="break-decrement"/>
<p id="session-label">Session length</p>
<Button onClick={this.handleClick} id="session-increment" />
<Button onClick={this.handleClick} id="session-decrement"/>
<Button onClick={this.handleTimer} id="start_stop"/>
<Button onClick={this.handleClick} id="reset"/>
</div>
)
}

This is a 'structural problem'.
You're checking condition in handler only once, not inside 'child function' invoked every second.
You need to move 'actions'/condition into 'interval body fn', sth like:
handleTimer(evt){
clearInterval(this.Interval)
this.Interval=setInterval(() => {
let timer=this.state.timer;
if(timer > 0){
this.setState({
timer: this.state.timer - 1
})
}else{
clearInterval(this.Interval)}
},1000)}
}

Related

setState not setting some states

Im creating a clock that has minutes and seconds. This is how my component looks like
class Time extends React.PureComponent
{
constructor(props)
{
super(props);
this.state =
{
hrs: 0,
mins: 0,
secs: 0
}
this.tick = this.tick.bind(this);
}
tick()
{
//This increments the minutes
if(this.state.secs >= 59)
{
this.setState({mins: this.state.mins+1});
this.setState({secs: 0});
}
//This increments the hours
if(this.state.minutes >= 59)
{
this.setState({hrs: this.state.hrs+1});
this.setState({mins: 0});
}
else
{
//This increments the seconds
this.setState({secs: this.state.secs+1});
}
}
componentDidMount()
{
this.tock = setInterval(()=>
{
this.tick();
}, 1000)
}
componentWillUnmount()
{
clearInterval(this.tock);
}
render()
{
return(
<div>
<h1>Time</h1>
<section>
{this.state.hrs}:{this.state.mins}:{this.state.secs}
</section>
</div>
);
}
}
}
export default Time;
The mins do increment to 59 and in turn increment the hrs - then reset to 0. But the secs just keep going and never reset to 0. They do update the mins though. The problem is that they go from 0 to infinity and dont stop counting. How can I make them reset to 0 each time?
setState is asynchronous, so you cannot ensure all the state updates in the sequence with multiple setState. I'd suggest that you should have only one setState at the end of that function after all secs, mins, and hrs computed.
Side note I also fixed your wrong value in this.state.minutes to this.state.mins.
class Time extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
hrs: 0,
mins: 0,
secs: 0,
};
this.tick = this.tick.bind(this);
}
tick() {
let { secs, mins, hrs } = this.state;
secs += 1;
if (secs > 59) {
mins += 1;
secs = 0;
}
if (mins > 59) {
hrs += 1;
mins = 0;
}
this.setState({ secs, mins, hrs });
}
componentDidMount() {
this.tock = setInterval(() => {
this.tick();
}, 1000);
}
componentWillUnmount() {
clearInterval(this.tock);
}
render() {
return (
<div>
<h1>Time</h1>
<section>
{this.state.hrs}:{this.state.mins}:{this.state.secs}
</section>
</div>
);
}
}
You could also use a functional based component to achieve the same effect.
function Time() {
const [time, setTime] = useState({
secs: 50,
mins: 58,
hrs: 0
});
const tick = () => {
setTime(prevState => ({...prevState, secs: prevState.secs + 1}));
}
useEffect(() => {
setInterval(() => {
tick()
}, 1000)
}, []);
useEffect(() => {
if(time.secs > 59) {
setTime(prevState => ({ ...prevState, secs: 0, mins: prevState.mins + 1 }));
}
}, [time.secs])
useEffect(() => {
if (time.mins > 59) {
setTime(prevState => ({ ...prevState, mins: 0, hrs: prevState.hrs + 1 }));
}
}, [time.mins])
return (
<div>
<h1>Time</h1>
<section>
{time.hrs}:{time.mins}:{time.secs}
</section>
</div>
);
}
export default Time;

Disabling button based on child component state in React

I am using a countdown component as a child component.
I want to disable/reable a button based on the state value of the counter, but I can't seem to read the value correctly.
This is what I have tried.
This is the countdown component:
import React from "react";
import PropTypes from "prop-types";
export default class Counter extends React.Component {
constructor() {
super();
this.state = { time: {}, seconds: 15 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
componentDidMount() {
let timeLeftVar = this.secondsToTime(this.state.seconds);
this.setState({ time: timeLeftVar });
}
startTimer() {
if (this.timer === 0 && this.state.seconds > 0) {
this.timer = setInterval(this.countDown, 1000);
} else if ((this.timer === 0 && this.state.seconds === 0)){
this.state.seconds = 15;
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
// Remove one second, set state so a re-render happens.
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
// Check if we're at zero.
if (seconds === 0) {
clearInterval(this.timer);
this.timer = 0;
console.log("counter is 0");
console.log(this.state.seconds);
console.log(this.timer);
}
}
render() {
this.startTimer();
return(
<span className={
this.state.seconds === 0 ? 'timerHidden' : 'timerActive'
}>
({this.state.time.s})
</span>
);
}
}
And how I read it and reset it in the parent component:
import Counter from '../Counter/Counter.js';
export default class Verify extends React.Component {
state = {
username: this.username,
email: this.email,
code: ""
};
constructor(props) {
super(props);
this.child = React.createRef();
}
resetTimer = () => {
this.child.current.startTimer();
};
resendConfirmationCode = async e =>{
this.resetTimer();
...
}
return (
<button
className="btn btn-primary register empty"
type="button"
disabled={this.child.current.seconds > 0}
onClick={this.resendConfirmationCode}>Resend code <Counter ref={this.child}/>
</button>
);
Inserting the counter works fine, reseting also, but the disabling of the button throws the following error:
TypeError: Cannot read property 'seconds' of null
Verify.render
> 109 | disabled={this.child.current.seconds > 0}
The this.child ref will be null/undefined on the initial render. Since you probably also want to disable the button if the counter component isn't available for some reason, you can just check if the ref's current value is falsey or if it is truthy and state.seconds of the child greater than 0.
<button
...
disabled={!this.child.current || this.child.current.state.seconds > 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />
If we invert the second condition we can combine them into a single comparison using Optional Chaining.
<button
...
disabled={!this.child.current?.state.seconds <= 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />

having trouble in getting data from local storage and displaying that data on the same page

As I'm new to ReactJS, I'm looking to get the data stored in the local storage and display it, basically its a timer, when we enter or set value in the input field, data is stored in the local storage so that on page reload it should'nt loose the data, now i want that data to be displayed in the fieldset present at the end of the code with id="lsOutput", which will get rendered only on button click of id="btnInsert" , any idea how to solve this?
This is the code
class Timer extends Component {
data;
constructor(props) {
super(props);
this.inputHandler = this.inputHandler.bind(this);
this.timerSubmit = this.timerSubmit.bind(this);
this.getData = this.getData.bind(this);
this.state = {
hours: 0,
minutes: 0,
seconds:0
}
this.hoursInput = React.createRef();
this.minutesInput= React.createRef();
this.secondsInput = React.createRef();
}
inputHandler = (e) => {
this.setState({[e.target.name]: e.target.value});
}
timerSubmit = (e) => {
e.preventDefault()
localStorage.setItem('key',JSON.stringify(this.state));
}
getData = (e) => {
e.preventDefault()
console.log( localStorage.getItem('key',JSON.parse(this.state)));
}
componentDidMount() {
this.data = JSON.parse(localStorage.getItem('key'));
if (localStorage.getItem('key')) {
this.setState({
hours: this.data.hours,
minutes: this.data.minutes,
seconds: this.data.seconds
})
} else {
this.setState({
hours: '',
minutes: '',
seconds: ''
})
}
}
convertToSeconds = ( hours, minutes,seconds) => {
return seconds + minutes * 60 + hours * 60 * 60;
}
startTimer = () => {
this.timer = setInterval(this.countDown, 1000);
}
countDown = () => {
const { hours, minutes, seconds } = this.state;
let c_seconds = this.convertToSeconds(hours, minutes, seconds);
if(c_seconds) {
// seconds change
seconds ? this.setState({seconds: seconds-1}) : this.setState({seconds: 59});
// minutes change
if(c_seconds % 60 === 0 && minutes) {
this.setState({minutes: minutes -1});
}
// when only hours entered
if(!minutes && hours) {
this.setState({minutes: 59});
}
// hours change
if(c_seconds % 3600 === 0 && hours) {
this.setState({hours: hours-1});
}
} else {
clearInterval(this.timer);
}
}
stopTimer = () => {
clearInterval(this.timer);
}
resetTimer = () => {
this.setState({
hours: 0,
minutes: 0,
seconds: 0
});
this.hoursInput.current.value = "00";
this.minutesInput.current.value = "00";
this.secondsInput.current.value = "00";
}
render() {
const { hours, minutes, seconds } = this.state;
const inphr = document.getElementById("inphr");
const inpmin = document.getElementById("inpmin");
const btnInsert = document.getElementById("btnInsert");
const lsOutput = document.getElementById("lsOutput");
window.onload = function(){
btnInsert.onclick = () => {
const key = inphr.value;
const value = inpmin.value;
if (key && value) {
localStorage.setItem(key, value);
window.localStorage.reload();
}
};
for (let i = 0; i < localStorage.length; i++ ){
const key = localStorage.key(i);
const value = localStorage.getItem(key);
lsOutput.HTML += `${key}: ${value}<br />`;
}
}
return (
<div className="App">
<div className="inputGroup" onChange={this.timerSubmit}>
<input id="inphr" className="timerinput" ref={this.hoursInput} type="text" placeholder={"00"} name="hours" onChange={this.inputHandler} /><span className="colan">:</span>
<input id="inpmin" className="timerinput" ref={this.minutesInput} type="text" placeholder={"00"} name="minutes" onChange={this.inputHandler} /><span className="colan">:</span>
<input id="inpsec" className="timerinput" ref={this.secondsInput} type="text" placeholder={"00"} name="seconds" onChange={this.inputHandler} />
</div>
<div className="timerbtn" >
<FontAwesomeIcon onClick={this.startTimer} className="start" icon={ faPlayCircle }/>
<FontAwesomeIcon onClick={this.stopTimer} className="stop" icon={ faPauseCircle }/>
<FontAwesomeIcon onClick={this.resetTimer} className="reset" icon={ faUndoAlt }/>
</div>
<h1 className="timercd" > {hours}:{minutes}:{seconds} </h1>
<button id="btnInsert">submit</button>
<label ref={this.hoursInput} id="labelhrs">
</label>
<fieldset>
<legend>
Timer
</legend>
<div id="lsOutput" >
</div>
</fieldset>
</div>
);
}
}
export default Timer;

How can I display one item from this array in the render?

I do not know of a way to display these conditions in the render. Nothing is appearing or if I try .map then it gives me errors.
I've tried refactoring, but came up with the same scenario. I've tried assigning the sampleInfo items to a new array but still can't figure out how to display just 1 item from that array in the render.
sampleInfo = [
{
second: 1,
ml: '19ml',
animal: 'Thing1',
weight: '4kg',
capacity: '20ml'
},
{
second: 2,
ml: '38ml',
animal: 'Thing2',
weight: '7kg',
capacity: '35ml'
},
{
second: 3,
ml: '57ml',
animal: 'Thing3',
weight: '12kg',
capacity: '60ml'
}
]
....Some functions here......
onSubmit(onetotwosec, morethantwosec) {
const {time} = this.state;
this.setState((state, props) => ({
scoreArray:state.scoreArray.concat(ms(state.time,{verbose: true})),
time:0,
submit:true
}));
// let myNewanimalArray = state.animalArray.concat({morethantwosec});
// console.log(myNewanimalArray)
let filterAnimal = 0
if((time >= 1000) && (time < 2000)) {
filterAnimal = this.sampleInfo.filter((item) => {
return item.second === 1;
}).map((item) => {
return item.animal
});
console.log(filterAnimal)
}
else if(time > 2000) {
filterAnimal = this.sampleInfo.filter((item) => {
return item.second === 2;
}).map((item) => {
return item.animal
});
console.log(filterAnimal)
}
}
render() {
if((this.state.isOn) === true){
return(
<React.Fragment>
<div>
<h3>Timer:{ms(this.state.time,{verbose: true})}</h3>
</div>
<div>
<button onMouseDown={this.onItemMouseDown} onMouseUp={this.onItemMouseUp}>start</button>
</div>
<div>
<ul>
{this.state.scoreArray.map(function(item,i){
return<li key={i}>{item}</li>
})}
</ul>
</div>
</React.Fragment>
)
} else if ((this.state.isOn) === false){
return(
<React.Fragment>
<div>
<h3>Timer:{ms(this.state.time)}</h3>
</div>
<div>
<button onMouseDown={this.onItemMouseDown} onMouseUp={this.onItemMouseUp}>Start</button><span></span>
<button onClick={this.resetTimer}>Reset</button>
<span></span>
<button onClick={this.onSubmit}>Done!</button>
</div>
<div>
<ul>
{this.state.scoreArray.map(function(item,i){
return<li key={i}>{item}</li>
})}
</ul>
//Here I've tried .map like above but can't figure out what to put!!!!
</div>
</React.Fragment>
I want the Done! button to display the matched animal from the sampleInfo array when the timer reaches a certain time. It should show each item as text such as 'Thing 1' if the timer is between 1 and 2 seconds etc. I can get it to display correctly using the console, but cannot for the life of me think of how to get it into the render. All of the functions are within one class.
I managed it myself (with help from google and all the above - this has taken me 3 days to do such a simple thing), here's the onSubmit function updated:
onSubmit() {
this.setState((state) => ({
scoreArray:state.scoreArray.concat(ms(state.time,{verbose: true})),
time:0,
submit:true
}));
const {time} = this.state;
var filterAnimal
if((time >= 1000) && (time < 2000)) {
filterAnimal = this.sampleInfo.filter((item) => {
return item.second === 1;
}).map((item) => {
return item.animal
}).toString()
this.setState(prevState => ({
animalArray:[filterAnimal,...prevState.animalArray ]})
)
}
And in the render:
{this.state.animalArray}
It works fine. If anyone can see a problem with this do comment.

Weird issue with setState and Callback

I'm coding a simple pomodoro timer which is fired using a click of the button.
tick() {
if (this.state.minutes > 0) {
setInterval(() => {
this.timer(this.state.minutes * 60 + this.state.seconds - 1);
}, 1000);
}
}
timer(x) {
this.setState(
{
minutes: Math.trunc(x),
seconds: (x*60) % 60
},
() => this.tick()
);
}
render() {
return (
<div className="App">
<div className="Pomodo">
<div className="counter">
{this.state.minutes + ":" + this.state.seconds}
</div>
<button className="btnCode" onClick={() => this.timer(25)}>
Code
</button>
<button className="btnCoffee" onClick={() => this.timer(5)}>
Coffee
</button>
</div>
</div>
);
}
}
export default App;
This shows the timer as follow:
25:00 (correct)
24:59 (correct)
24:57 == wrong should be 58
24:53 == wrong should be 57
...etc
What am I missing here please? When troubleshooting via chrome the counter is fine and shows the correct numbers.
It is skipping the seconds because, everytime you call tick(), you are setting a new interval by calling this.tick() in setState. You can fix this by adding a flag after calling tick for the first time.
One more issue I see is, in the btnCode's onClick, you are passing 25 as minutes to this.timer. But, in setInterval, you are calling timer with everything seconds. I suggest you pass everything in seconds to timer function. Also, it is good to clear intervals at unmounting.
See the adjusted code.
class TodoApp extends React.Component {
constructor(props) {
super(props)
this.state = {
minutes: 0,
seconds: 0,
started: false
}
this.tick = this.tick.bind(this);
this.timer = this.timer.bind(this);
}
tick() {
// Add flag so that tick is not called again
this.setState({
started: true
})
if (this.state.minutes > 0) {
this.interval = setInterval(() => {
const seconds = this.state.minutes * 60 + this.state.seconds - 1;
this.timer(seconds);
}, 1000);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
timer(x) {
this.setState(
{
minutes: Math.trunc(x / 60),
seconds: x % 60
},
() => !this.state.started && this.tick() // only call if its timer is not started before
);
}
render() {
return (
<div className="App">
<div className="Pomodo">
<div className="counter">
{this.state.minutes + ":" + this.state.seconds}
</div>
<button className="btnCode" onClick={() => this.timer(25 * 60)}>
Code
</button>
<button className="btnCoffee" onClick={() => this.timer(5 * 60)}>
Coffee
</button>
</div>
</div>
);
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Resources