0
votes

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>
    );
};
1
Can you share some sample code in which your problem is reproducible? It's very hard to deduce what your problem is, given the current state of your question.maazadeeb
Consider adding some sample code to give your question more context, and to help the community answer your question more effectively.Dacre Denny
added some codeKevin Chu

1 Answers

0
votes

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.