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>
);
};