William
William

Reputation: 4588

How to toggle setInterval in a child via state that was passed from parent component as a prop

Complete working example at jsbin: http://jsbin.com/tepedepozi/1/edit?console,output

_____________________________________________________________--

I have a component named App and the full returned JSX is here:

 <main style = {mainContainer}>  
   <Loader/>
   <section style={goldPanel}>
     <TimeDisplay isPlaying = {this.state.isPlaying}/>
     <div className="buttonContainer">
     <Button buttonText = {this.state.recordButtonText} onClick={this.toggleRecording}/> 
     <Button buttonText = {this.state.playButtonText} onClick={this.toggleSongPlaying} />
      </div>
   </section>
 </main>

Notice there are two buttons: a record button and a play button

When then the user clicks the play button:

<Button buttonText = {this.state.playButtonText} onClick={this.toggleSongPlaying} />

a method is invoked that changes this.state.isPlaying to true:

If the user clicks th record button it does not change this.state.isPlaying at all - ever ! This is how I programmed it and it is the behavior I want !

In the TimeDisplay component I pass the state of this.state.isPlaying as a prop.

<TimeDisplay isPlaying = {this.state.isPlaying}/>

The problem is that I want to program TimeDisplay to behave in ways that are based on this state, and the TimeDisplay component should only respond to the state change based on users clicking:

<Button buttonText = {this.state.playButtonText} onClick={this.toggleSongPlaying} />

However, when the user clicks the play button and then the record button the record button forces a rerender on the TimeDisplay component. I demonstrate the problem by placing a setInterval in the startCount() method code below.

The TimeDisplay component:

class TimeDisplay extends React.Component {

    constructor(props) {
        super(props)

    }

    startCounter(){

        if(this.props.isPlaying){
            console.log("counter working");  // runs no matter what button is clicked after play button
            setInterval(()=>{                
                console.log("Multiple setIntervals get invoked when user clicks record...not good"); 
            },1000)
        }
    }



    render() {

        this.startCounter()                //______Due to rerender. How to fix ? 

        return (
            <div>   
            <p><span id="seconds">00</span>:<span id="tens">00</span></p>
          </div>
        )
    }

}

Upvotes: 1

Views: 1100

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 281892

You need to write this.startCounter() function in your componentDidMount method and not in the render since the setInterval will do the job for you and you only have to call the setInterval once and not on every render. Now that isPlaying is received from props, you need to also update your child counter. You can do that in componentDidUpdate like

class TimeDisplay extends React.Component {

    constructor(props) {
        super(props)
        this.interval = null;

    }

    componentDidMount() {
        this.startCounter();

    }

    componentDidUpdate(prevProps) {
        if(prevProps.isPlaying !== this.props.isPlaying) {
            this.startCounter();
        }

    }
    startCounter() {

        if (this.props.isPlaying) {

            this.interval = setInterval(() => {
                console.log("Multiple setIntervals get invoked when user clicks record...not good");
            }, 1000);

        }else{

           clearInterval(this.interval);
        }
    }


    render() {


        return (
            <div>   
            <p><span id="seconds">00</span>:<span id="tens">00</span></p>
          </div>
        )
    }

}

class Button extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        return (<button onClick={this.props.onClick}>{this.props.buttonText}</button>)
    }
}

Working JSBIN

P.S. I have used componentDidUpdate instead of componentWillReceiveProps because React has proposed that they will rename componentWillReceiveProps from v16.3.0 onwards and remove it from v17. Check this conversation

Upvotes: 1

Ved
Ved

Reputation: 12103

You need to cancel the interval before triggering new. Also @shubham suggested do call startCounter method like he suggested.

  constructor(props) {
        super(props)
     this.timeInterval = null; 
      }
   }

   componentWillUnmount(){
           if(this.timeInterval != null){
            clearInterval(this.timeInterval)// Clear interval on page unmount
      }
    }

   startCounter(){
         if(this.timeInterval != null){
                clearInterval(this.timeInterval)// Cancel interval if it is being executing.
          }
            if(this.props.isPlaying){

                console.log("counter working");  // runs no matter what button is clicked after play button
          this.timeInterval  = setInterval(()=>{                
                    console.log("Multiple setIntervals get invoked when user clicks record...not good"); 
                },1000)
            }
        }

   componentDidMount() {
     this.startCounter()       // Call on initial render   
   }

   componentWillReceiveProps(){
      this.startCounter() //call on props change
     }

    render() {
        return (
            <div>   
            <p><span id="seconds">00</span>:<span id="tens">00</span></p>
          </div>
        )
    }

}

Upvotes: 0

Related Questions