React count down error - javascript

I have implemented a vanilla js countdown into a react component as follow:
import React, { Component } from 'react';
class CustomCountDown extends Component {
constructor(props) {
super(props);
this.endTime;
this.msLeft;
this.time;
this.hours;
this.mins;
this.element;
}
twoDigits( n ){
return (n <= 9 ? "0" + n : n);
}
updateTimer() {
this.msLeft = this.endTime - (+new Date);
if (this.msLeft < 1000 ) {
element.innerHTML = "countdown's over!";
} else {
this.time = new Date(this.msLeft );
this.hours = this.time.getUTCHours();
this.mins = this.time.getUTCMinutes();
this.element.innerHTML = (this.hours ? this.hours + ':' + this.twoDigits( this.mins ) : this.mins) + ':' + this.twoDigits( this.time.getUTCSeconds() );
setTimeout( this.updateTimer, this.time.getUTCMilliseconds() + 500 );
}
}
countdown( elementName, minutes, seconds ) {
this.element = document.getElementById( elementName );
this.endTime = (+new Date) + 1000 * (60*minutes + seconds) + 500;
this.updateTimer();
}
componentDidMount() {
this.countdown("count", 1, 30);
}
render() {
return(
<div id="count">
</div>
);
}
}
export default CustomCountDown;
I can't figure out why I am getting the following error:

When you pass this.updateTimer to setTimeout you loose context, i.e. this no longer points to your component instance. You need to keep the context either way:
setTimeout( this.updateTimer.bind(this), this.time.getUTCMilliseconds() + 500 );
setTimeout( () => this.updateTimer(), this.time.getUTCMilliseconds() + 500 );
As a better alternative, you can bind updateTimer in the constructor. This won't create new function every time updateTimer is called:
constructor(props) {
// ...
this.updateTimer = this.updateTimer.bind(this);
}

Related

Uncaught TypeError: timer.getTime is not a function at update

Why does my class declaration give the error "Uncaught TypeError: timer.getTime is not a function at update"? My other class functions, like "timer.getStop()", does not. Why is that?
class Timer {
constructor() {
this.time = 0;
this.stop = false;
setInterval(update, 1000);
}
setStop(bool) {
this.stop = bool;
}
setTime(t) {
this.time = t;
}
getStop() {
return this.stop;
}
getTime() {
return this.time;
}
}
function update() {
if (timer.getTime() != 60 && !timer.getStop()) {
timer.setTime(timer.getTime += 1)
if (timer.getTime() <= 60) {
clock.innerHTML = 60 - timer.getTime();
}
//Update Stats
grossWPM = (entries / 5) / (time / 60);
netWPM = grossWPM - (wrong / (time / 60));
gross.innerHTML = 'Gross WPM: ' + grossWPM.toFixed(0);
net.innerHTML = 'Net WPM: ' + netWPM.toFixed(0);
}
}
Try to fix this
timer.setTime(timer.getTime += 1)
like that
timer.setTime(timer.getTime() + 1)

React child updates change parent scroll position

I have a React container component that is suppose to house a list of child timer components. These child components are just countdown timers.
To achieve this my child component class uses setInterval to update the child every second. As the updates happened, I noticed as I scrolled down or up the list, the container component would suddenly jump up or down in a noticeably big way exactly when the child timers update themselves, but the container's render method is never called.
Along with this, if I simply don't scroll or stop scrolling, the jumping never happens. If I scrolled and then stopped scrolling, the onscroll handler keeps firing in sync with the child timer updates even though I stopped scrolling.
I've never encountered a situation like this before. Can React children implicitly force their parent container to randomly scroll up and down when updating?
Here is the code for the container:
class UserContentDetail extends React.Component {
constructor(props) {
super(props);
this.state ={
page: 1,
perPage: 15,
animes: [],
currTab: props.currTab
};
this.startApiRequests = this.startApiRequests.bind(this);
this.handleResponse = this.handleResponse.bind(this);
this.handleData = this.handleData.bind(this);
this.handleError = this.handleError.bind(this);
}
componentDidMount() {
this.startApiRequests();
}
componentDidUpdate() {
if (this.props.currTab !== this.state.currTab) {
this.startApiRequests();
}
}
startApiRequests() {
let currSeason = anilistApiConstants.SEASON_SUMMER;
let currSeasonYear = 2018;
let resultPromise = null;
switch(this.props.currTab) {
case sideNavConstants.SIDE_NAV_TAB_MY_ANIME:
resultPromise = api.getMyAnimes(this.props.myAnimeIds, this.state.page, this.state.perPage);
break;
case sideNavConstants.SIDE_NAV_TAB_POPULAR_ANIME:
resultPromise = api.getPopularAnimes(this.state.page, this.state.perPage);
break;
case sideNavConstants.SIDE_NAV_TAB_NEW_ANIME:
resultPromise = api.getNewAnimes(currSeason, currSeasonYear, this.state.page, this.state.perPage);
break;
}
resultPromise.then(this.handleResponse)
.then(this.handleData)
.catch(this.handleError);
}
handleResponse(response) {
return response.json().then(function (json) {
return response.ok ? json : Promise.reject(json);
});
}
handleData(data) {
let results = data.data.Page.media;
for (let i = 0; i < results.length; ++i) {
if (results[i].nextAiringEpisode == null) {
results[i].nextAiringEpisode = {empty: true};
}
}
this.setState({
page: 1,
perPage: 15,
animes: results,
currTab: this.props.currTab
});
}
handleError(error) {
alert('Error, check console');
console.error(error);
}
render() {
console.log('rendering list');
return(
<div className={userMasterDetailStyles.detailWrapper}>
<div className={userMasterDetailStyles.detailList}>
{this.state.animes.map(anime => <AnimeCard {...anime} key={anime.id} />)}
</div>
</div>
);
}
}
Here is the code for my timers (AnimeCardTime) and which is surrounded by a card container (AnimeCard):
class AnimeCardTime extends React.Component {
constructor(props) {
super(props);
this.state = {
timeUntilNextEpisode: props.timeLeft,
animeId: props.id
};
this.countdownTimer = null;
this.getNextEpisodeTimeUntilString = this.getNextEpisodeTimeUntilString.bind(this);
this.startTimer = this.startTimer.bind(this);
this.endTimer = this.endTimer.bind(this);
}
componentDidMount() {
this.startTimer();
}
componentWillUnmount() {
this.endTimer();
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.animeId) {
return {
timeUntilNextEpisode: nextProps.timeLeft,
animeId: nextProps.id
};
}
return null;
}
startTimer() {
if (this.props.timeLeft != undefined) {
this.countdownTimer = setInterval(() => {
this.setState({
timeUntilNextEpisode: this.state.timeUntilNextEpisode - 60,
});
}, 1000);
}
}
endTimer() {
if (this.countdownTimer != null) {
clearInterval(this.countdownTimer);
}
}
secondsToTimeString(timeSecondsUntil) {
timeSecondsUntil = Number(timeSecondsUntil);
let d = Math.floor(timeSecondsUntil / (3600*24));
let h = Math.floor(timeSecondsUntil % (3600*24) / 3600);
let m = Math.floor(timeSecondsUntil % (3600*24) % 3600 / 60);
let dDisplay = d > 0 ? d + 'd ' : '';
let hDisplay = h > 0 ? h + 'hr ' : '';
let mDisplay = m > 0 ? m + 'm ' : '';
return dDisplay + hDisplay + mDisplay;
}
getNextEpisodeTimeUntilString() {
if (this.props.timeLeft != undefined) {
return 'Ep ' + this.props.nextEpisode + ' - ' + this.secondsToTimeString(this.state.timeUntilNextEpisode);
}
else {
return this.props.season + ' ' + this.props.seasonYear;
}
}
render() {
return(<h6 className={userMasterDetailStyles.cardTime}>{this.getNextEpisodeTimeUntilString()}</h6>);
}
}
const AnimeCard = (props) => {
let secondsToTimeString = (timeSecondsUntil) => {
timeSecondsUntil = Number(timeSecondsUntil);
let d = Math.floor(timeSecondsUntil / (3600*24));
let h = Math.floor(timeSecondsUntil % (3600*24) / 3600);
let m = Math.floor(timeSecondsUntil % (3600*24) % 3600 / 60);
let dDisplay = d > 0 ? d + 'd ' : '';
let hDisplay = h > 0 ? h + 'hr ' : '';
let mDisplay = m > 0 ? m + 'm ' : '';
return dDisplay + hDisplay + mDisplay;
};
let getNextEpisodeTimeUntilString = () => {
if (props.status === anilistApiConstants.STATUS_RELEASING) {
return 'Ep ' + props.nextAiringEpisode.episode + ' - ' + secondsToTimeString(props.nextAiringEpisode.timeUntilAiring);
}
else {
return props.season + ' ' + props.startDate.year;
}
};
return(
/* <h6 className={userMasterDetailStyles.cardTime}>{getNextEpisodeTimeUntilString()}</h6> */
<a className={userMasterDetailStyles.animeCardLinkContainer} href={props.siteUrl}>
<div className={userMasterDetailStyles.animeCardContainer}>
<h6 className={userMasterDetailStyles.cardTitle}>{props.title.romaji}</h6>
<AnimeCardTime timeLeft={props.nextAiringEpisode.timeUntilAiring} nextEpisode={props.nextAiringEpisode.episode} season={props.season} seasonYear={props.startDate.year} id={props.id}/>
<img className={userMasterDetailStyles.cardImage} src={props.coverImage.large}/>
<p className={userMasterDetailStyles.cardDescription}>{props.description.replace(/<(?:.|\n)*?>/gm, '')}</p>
<p className={userMasterDetailStyles.cardGenres}>{props.genres.reduce((prev, curr) => {return prev + ', ' + curr;})}</p>
</div>
</a>
);
};
I realized the problem was more with the css rather than the react code. I had not set an explicit height for the container and I assume it was this uncertainty that caused the browser to suddenly scroll up and down the container whenever the list elements re-rendered/updated themselves.

How to stop a second React timer from looping back from zero?

I'm trying to build a React component that has two timers. The first one is three seconds, and when it hits zero it says "GO!" then immediately starts the next one, which is a 60 second timer. I got all of this to work, but I can't figure out how to stop the 60 second timer from starting over when it hits zero. Ideally I will be splitting these into two components once I figure this out, but right now I just want to get everything to work properly. Can someone steer me in the right direction here?
import React, { Component } from "react";
import ReactDOM from "react-dom"
class Countdown extends React.Component {
constructor(props) {
super(props);
this.state = { time: {}, seconds: 3 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countdown = this.countdown.bind(this);
}
formatSeconds (totalSeconds) {
let seconds = totalSeconds;
let minutes = Math.floor(totalSeconds / 60);
if (seconds < 10) {
seconds = '0' + seconds;
}
if (minutes < 10) {
minutes = '0' + minutes;
}
let obj = {
"minutes": minutes,
"seconds": seconds
};
return obj;
}
componentDidMount() {
let timeLeft = this.formatSeconds(this.state.seconds);
this.setState({ time: timeLeft });
}
startTimer() {
if (this.timer === 0) {
this.timer = setInterval(this.countdown, 1000);
}
}
countdown() {
let seconds = this.state.seconds - 1;
this.setState({
time: this.formatSeconds(seconds),
seconds: seconds
});
if (seconds === 0) {
clearInterval(this.timer);
let element = (
<h2>GO!</h2>
);
ReactDOM.render(
element,
document.getElementById('go')
);
this.state.seconds = 61;
let seconds = this.state.seconds - 1;
this.timer = setInterval(this.countdown, 1000);
this.setState({
time: this.formatSeconds(seconds),
seconds: seconds
});
// how do I stop the second timer from looping?
}
}
render() {
return(
<div className="container">
<div className="row">
<div className="column small-centered medium-6 large-4">
<h2 id="go"></h2>
{this.state.time.seconds}<br />
<button className="button" onClick={this.startTimer}>Start</button>
</div>
</div>
</div>
);
}
}
export default Countdown;
Could you break it out into two countdown functions as follows?
class Countdown extends React.Component {
constructor(props) {
super(props);
this.state = {
countdownSeconds: 3,
roundSeconds: 60
};
this.countdown = this.countdown.bind(this);
this.roundCountdown = this.roundCountdown.bind(this);
this.startTimer = this.startTimer.bind(this);
}
startTimer() {
if(!this.countdownTimer) {
this.countdownTimer = setInterval(this.countdown, 1000);
}
}
countdown() {
const currentSeconds = this.state.countdownSeconds - 1;
this.setState({
countdownSeconds: currentSeconds
});
if(currentSeconds === 0) {
this.roundTimer = setInterval(this.roundCountdown, 1000);
clearInterval(this.countdownTimer);
}
}
roundCountdown() {
const currentSeconds = this.state.roundSeconds - 1;
this.setState({
roundSeconds: currentSeconds
});
if(currentSeconds === 0) {
//do whatever
clearInterval(this.roundTimer);
}
}
}
Edit:
If I understand your other question, to display the appropriate countdown you could maybe do something like
render() {
const { countdownSeconds, roundSeconds } = this.state;
return (
<div>
{countdownSeconds > 0 &&
<span>Counting down...{countdownSeconds}</span>
}
{roundSeconds > 0 && countdownSeconds === 0 &&
<span>Round underway...{roundSeconds}</span>
}
</div>
);
}
Or you could functionalize the rendering of the the timer, i.e.
renderTimer() {
const { countdownSeconds, roundSeconds } = this.state;
if(countdownSeconds > 0) {
return <span>Counting down...{countdownSeconds}</span>;
}
return <span>Round underway...{roundSeconds}</span>;
}
and then:
render() {
return (
<div>
{this.renderTimer()}
</div>
);
}

Javascript not able to make common functions for a prototype

I am trying to make timer in javascript using a prototype. Each time a new timer is created, a object of prototype is created. There are methods to increase time and print each second. The whole code snippet is as follows:
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.prototype.start = function() {
var self = this;
if (!self.interval) {
self.interval = setInterval(update, 1000);
}
function update() {
incrementTime();
render();
}
function render() {
self.elem.innerText = getPrintableTime();
}
function incrementTime() {
self.currentTime["min"] += Math.floor((++self.currentTime["sec"]) / 60);
self.currentTime["hr"] += Math.floor(self.currentTime["min"] / 60);
self.currentTime["sec"] = self.currentTime["sec"] % 60;
self.currentTime["min"] = self.currentTime["min"] % 60;
}
function getPrintableTime() {
var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
};
module.exports = Timer;
I have all methods in start function. The problem is that for each new object of Timer, new space for each method will be used which is very inefficient. But when I try to put methods outside of start function, they lose access to self variable. You can see that there is setInterval function used which will be calling these methods per second. I cannot use this also as this will be instance of Window in subsequent calls.
How can I solve this situation by only keeping one instance of all the interior methods?
You don't need to have all methods in the start function. Yes, for each new Timer instance, new space for each function will be used, but that is necessary when you want to work with setInterval as you need a function which closes over the instance. However, you need only one such closure, the other methods can be standard prototype methods.
function getTwoDigitNumber(number) {
return (number > 9 ? "" : "0") + number;
}
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.prototype.start = function() {
var self = this;
if (!this.interval) {
this.interval = setInterval(function update() {
self.incrementTime();
self.render();
}, 1000);
}
};
Timer.prototype.render() {
this.elem.innerText = this.getPrintableTime();
};
Timer.prototype.incrementTime = function() {
this.currentTime.sec += 1;
this.currentTime.min += Math.floor(this.currentTime.sec / 60);
this.currentTime.hr += Math.floor(this.currentTime.min / 60);
this.currentTime.sec = this.currentTime.sec % 60;
this.currentTime.min = this.currentTime.min % 60;
};
Timer.prototype.getPrintableTime = function() {
var text = getTwoDigitNumber(this.currentTime.hr) + ":"
+ getTwoDigitNumber(this.currentTime.min) + ":"
+ getTwoDigitNumber(self.currentTime.sec);
return text;
};
module.exports = Timer;
Btw, regarding your incrementTime pattern, you should have a look at How to create an accurate timer in javascript?.
You can use apply to use functions defined outside of prototype with correct this context.
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
function update() {
incrementTime.apply(this);
render.apply(this);
}
function render() {
this.elem.innerText = getPrintableTime.apply(this);
}
function incrementTime() {
this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
this.currentTime["sec"] = this.currentTime["sec"] % 60;
this.currentTime["min"] = this.currentTime["min"] % 60;
}
function getPrintableTime() {
var text = getTwoDigitNumber(this.currentTime["hr"]) + ":" + getTwoDigitNumber(this.currentTime["min"]) + ":" + getTwoDigitNumber(this.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
Timer.prototype.start = function() {
var self = this;
if (!self.interval) {
self.interval = setInterval(function() {
update.apply(self);
}, 1000);
}
};
document.addEventListener('DOMContentLoaded', function() {
var timer = new Timer(document.getElementById('timer'));
timer.start();
}, false);
<div id="timer"></div>
If I understand correctly, you're wanting to only create one interval.
One possible solution would be to create a static method and variable to manage the setInterval. I would note that while this may be more performance friendly, the timers will always start and run on the same count...not from the moment each timer is created. (See example)
Of course, you could capture the current timestamp and calculate the elapsed time from there. But, that's another thread ;)
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.subscribe = function(timer) {
Timer.subscribers = Timer.subscribers || [];
if (Timer.subscribers.indexOf(timer) === -1) {
Timer.subscribers.push(timer);
timer.update.call(timer);
}
Timer.checkInterval();
};
Timer.unsubscribe = function(timer) {
Timer.subscribers = Timer.subscribers || [];
if (Timer.subscribers.indexOf(timer) !== -1) {
Timer.subscribers.splice(Timer.subscribers.indexOf(timer), 1);
}
Timer.checkInterval();
};
Timer.checkInterval = function() {
if (!Timer.interval && Timer.subscribers.length > 0) {
Timer.interval = setInterval(function() {
Timer.subscribers.forEach(function(item) {
item.update.call(item);
});
}, 1000);
} else if (Timer.interval && Timer.subscribers.length === 0) {
clearInterval(Timer.interval);
Timer.interval = null;
}
};
Timer.prototype = {
start: function() {
Timer.subscribe(this);
},
stop: function() {
Timer.unsubscribe(this);
},
update: function() {
this.incrementTime();
this.render();
},
incrementTime: function() {
this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
this.currentTime["sec"] = this.currentTime["sec"] % 60;
this.currentTime["min"] = this.currentTime["min"] % 60;
},
render: function() {
var self = this;
function getPrintableTime() {
var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
this.elem.innerText = getPrintableTime();
}
};
/**
*
*/
var timers = document.getElementById('timers');
function addTimer() {
var el = document.createElement('div');
var tmr = document.createElement('span');
var btn = document.createElement('button');
var t = new Timer(tmr);
btn.innerText = 'Stop';
btn.onclick = function() {
t.stop();
};
el.appendChild(tmr);
el.appendChild(btn);
timers.appendChild(el);
t.start();
};
<div id="timers"></div>
<button onclick="addTimer()">Add Timer</button>

JavaScript Timer: Code Working Uncaught TypeError

I am using a javascript timer on a page, which redirects the user after a finite time. The code is doing exactly what I want it to but it continues to throw errors in the console. The error message I get is
Uncaught TypeError: Cannot set property 'textContent' of null
//Countdown Timer
function CountDownTimer(duration, granularity) {
this.duration = duration;
this.granularity = granularity || 1000;
this.tickFtns = [];
this.running = false;
}
CountDownTimer.prototype.start = function() {
if (this.running) {
return;
}
this.running = true;
var start = Date.now(),
that = this,
diff, obj;
(function timer() {
diff = that.duration - (((Date.now() - start) / 1000) | 0);
if (diff > 0) {
setTimeout(timer, that.granularity);
} else {
diff = 0;
that.running = false;
}
obj = CountDownTimer.parse(diff);
that.tickFtns.forEach(function(ftn) {
ftn.call(this, obj.minutes, obj.seconds);
}, that);
}());
};
CountDownTimer.prototype.onTick = function(ftn) {
if (typeof ftn === 'function') {
this.tickFtns.push(ftn);
}
return this;
};
CountDownTimer.prototype.expired = function() {
return !this.running;
};
CountDownTimer.parse = function(seconds) {
return {
'minutes': (seconds / 60) | 0,
'seconds': (seconds % 60) | 0
};
};
window.onload = function() {
var display = document.querySelector(".cdtimer"),
s6timer = new CountDownTimer(20);
s6timer.onTick(format).onTick(redirect).start();
function redirect() {
if (s6timer.expired()) {
window.location.replace("../assessmentportal/sectiontimeout.php");
}
};
function format(minutes, seconds) {
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.textContent = minutes + ':' + seconds;
};
};
});
This happens because this call document.querySelector(".cdtimer") returns null. There are 2 possible reasons:
There are no element with class name cdtimer
You are running your script before DOM loaded
Is your assignment of display actually returning a DOM element? If it can't find anything (typo, perhaps?) it will return null.

Categories

Resources