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>
Related
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} />
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;
The first time the Test component is navigated to, everything works correctly. React Navigation 5 is used to navigate to the Results page, and then navigate back to the Test component. However, although I receive the timerIsRunning prop correctly via the route.params this is not set and passed to the <Timer> as I would expect.
Have I misunderstood how this is meant to work?
Test component (abridged)
export const Test = ({ navigation, route }) => {
const { timed, category, minutes, timerIsRunning } = route.params; // data is received correctly
const [timerRunning, setTimerRunning] = React.useState(timerIsRunning); // this works the first time it's navigated to
return (
<View style={{ flex: 1, margin: 10 }}>
<ScrollView>
{timed ? <Timer seconds={minutes*60} timerRunning={timerRunning} /> : null} // timer component called here
</ScrollView>
<Button
onPress={() => {
setResponseDetails({ answered: false, index: null });
setActiveIndex(0);
setStoredResponses([]);
setTimerRunning(false); // this correctly stops the timer
navigation.navigate('Results', { // and navigates to the Results page
data: storedResponses,
category: category,
timed: timed,
minutes: minutes,
});
}}
disabled={!responseDetails.answered}
style={styles.button}
>
<Text>Results</Text>
<Icon name="arrow-dropright" />
</Button>
)
</View>
);
};
Results page (abridged)
export const Results = ({ navigation, route }) => {
const { category, data: testData, timed, minutes } = route.params;
return (
<View>
<Button
onPress={() =>
navigation.navigate('Test', { // Navigates back to test
timed: timed,
minutes: minutes,
category: category,
timerIsRunning: true // Correctly passes this prop, and is received in the Test component
})
}
>
<Icon name="arrow-back" />
<Text>Try the {category} test again</Text>
</Button>
</View>
);
};
Have you tried adding timerActive as one of your dependencies on the second useEffect?
React.useEffect(() => {
if (timeLimit > 0 && timerActive) {
setTimeout(() => {
// console.log('startTime, ', timeLimit);
settimeLimit(timeLimit - 1);
}, 1000);
}
if (timeLimit === 0 && timerRunning || !timerRunning) {
console.log('done');
}
}, [timeLimit, timerActive]);
Here is an example of a working timer that takes in seconds and timerRunning as props, please let me know if you need any help implementing it. You can update your question or add a working snippet.
const pad = (num) => `0${String(num)}`.slice(-2);
//moved formatTime out of the component
const formatTime = (timeLimit) => {
let displaySeconds = timeLimit % 60;
let minutes = Math.floor(timeLimit / 60);
//clean up formatting number
return `${pad(minutes)}:${pad(displaySeconds)}`;
};
const Timer = ({ seconds, timerRunning }) => {
const [timeLimit, settimeLimit] = React.useState(seconds);
//removed timerActive as it's a copy of timerRunning
React.useEffect(
() => {
if (timerRunning) {
//changed this to a interval and cancel it
// when timeLimit or timerRunning changes
let interval;
if (timerRunning) {
interval = setInterval(() => {
//using callback to get current limit value
// prevent using stale closure or needless
// dependency
settimeLimit((timeLimit) => {
//do the check here so timeLimit is not
// a dependency of this effect
if (timeLimit !== 0) {
return timeLimit - 1;
}
//do not keep running the interval
// if timeLimit is 0
clearInterval(interval);
return timeLimit;
});
}, 1000);
}
//clean up function will end interval
return () => clearInterval(interval);
}
},
//re run if seconds or timerRunning changes
[seconds, timerRunning]
);
//set local timeLimit again when seconds change
React.useEffect(() => settimeLimit(seconds), [seconds]);
return <div> {formatTime(timeLimit)}</div>;
};
function App() {
const [seconds, setSeconds] = React.useState(10);
const [running, setRunning] = React.useState(false);
return (
<div>
<div>
<label>
run timer
<input
type="checkbox"
checked={running}
onChange={() => setRunning((r) => !r)}
/>
</label>
<div>
<label>
<select
onChange={(e) =>
setSeconds(Number(e.target.value))
}
value={seconds}
>
<option value={1}>1</option>
<option value={10}>10</option>
<option value={100}>100</option>
<option value={1000}>1000</option>
</select>
</label>
</div>
</div>
<Timer seconds={seconds} timerRunning={running} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Okay, actually I worked this out - with React Navigation 5.x you can use the route.params in the useEffect to trigger an action. I ended up with this:
React.useEffect(() => {
setTimerRunning(timerIsRunning);
}, [route.params]);
As part of my React learning, I wrote a simple timer. After the timer reaches the end (with useEffect) it doesn't enter the function buttonString after the useEffect hook, because the measurement isn't part of the application state, so the HTML isn't rendered.
What is the correct way to handle such a situation? Inserting the setInterval's id to a hook sounds me incorrect, but I'm not sure.
Thank you
The code:
var measurement;
export default function App() {
const [timeLeft, setTimeLeft] = useState("10000");
useEffect(() => {
if (timeLeft<500) {
clearInterval(measurement);
measurement = null;
}
});
function buttonString() {
console.log('[buttonString] measurement:' + measurement);
return ((measurement!==null) && (measurement!==undefined)) ? "reset" : "Click To Start";
}
function timeLeftAsString() {
let duration = moment.duration(timeLeft);
let mins = duration.minutes();
let secs = duration.seconds();
return mins + " : " + secs;
}
function startMeasurement() {
clearInterval(measurement);
var timeStart = moment().add(11, 's');
measurement = setInterval(() => {
setTimeLeft(moment(timeStart).diff());
}, 500);
}
return (
<div className="App">
<p> {timeLeftAsString()} </p>
<Button variant="primary" size="lg" className="Button" onClick={() => {
startMeasurement();
}}>
{buttonString()}
</Button>
</div>
);
}
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)}
}