Search code examples
javascripthtmlreactjssetstate

SetState callback gets called before state is mutated in react


The following is the code breakdown leading to the part where I run into trouble.

I defined a class component to store the state data and the "timerState" , which is the main focus in this case, will be toggled between true and false

   this.state={
      brkLength:1,
      sesnLength:1,
      timer:60,
      timerState:'false',
      timerType:'Session',
    }

The handleTimer function will be fired up once the onclick event takes place. Since setstate runs asynchronously and I don't want the function-timeCountDown and breakCountDown to be called before state mutation, I set them as callback function of setState.

handleTimer(){
  console.log(this.state.timerState)
  if(this.state.timerType=="Session"){
  this.setState({
    timerState:!this.state.timerState
  },()=>this.timeCountDown())
  }else if(this.state.timerType=="Break"){
  this.setState({
    timerState:!this.state.timerState
  },()=>this.breakCountDown()) 
  }
}

However, as the console.log show in two places-one in handleTimer and the other in timeCountDown, both of them print "false".

timeCountDown(){
  console.log(this.state.timerState)
  if(this.state.timerState){
    this.myCountDown=setInterval(()=>{
      if(this.state.timer>0){
       this.setState(prevState=>({
       timer:prevState.timer-1
     }))
      }else if(this.state.timer<=0){
        clearInterval(this.myCountDown)
        this.soundPlay()
        this.setState({
          timerType:'Break',
          timer:this.state.brkLength*60,
        },()=>this.breakCountDown())      
      }
    }
   ,1000)
  }else{
    clearInterval(this.myCountDown)
  }
}

I wonder what goes wrong above in the code snippet. Here comes the link to see the entire coding if you want to look into it.

function formatTime(time){
  let minutes=Math.floor(time/60)
  let seconds=time%60
  minutes=minutes<10?"0"+minutes:minutes
  seconds=seconds<10?"0"+seconds:seconds
  return minutes +":"+seconds
}


const TimerLengthControl=(props)=>(
  <div className="LengthContainer">
    <div className="controlTitle" id={props.titleID}>{props.title}</div>
    <div>
      <button 
        id={props.decrementID} 
        value="-1" 
        type={props.type} 
        onClick={props.onClick}
        >
      <i className="fas fa-arrow-down"></i>
      </button>
      <span id={props.spanID}>{props.span}</span>
      <button 
        id={props.incrementID} 
        value="+1" 
        type={props.type}
        onClick={props.onClick}
        >
      <i className="fas fa-arrow-up"></i>
      </button>
    </div>
  </div>  
 )

const TimerControl=(props)=>(
  <div className="timerControlContainer">
    <div className="timerContainer">
      <div id="timer-label">{props.timerType}</div>
      <div id="time-left">{formatTime(props.timeLeft)}</div>
    </div>
    <div className="buttonContainer">
      <button id="start_stop" onClick={props.timerHandler}>
        <i className="fas fa-play"/>
        <i className="fas fa-pause"/>
      </button>
      <button id="reset">
        <i className="fas fa-sync" onClick={props.resetHandler}/>
      </button>
    </div>
  </div>
)

class App extends React.Component{
  constructor(){
    super()
    this.state={
      brkLength:1,
      sesnLength:1,
      timer:60,
      timerState:'false',
      timerType:'Session',
    }
    this.handleReset=this.handleReset.bind(this)
    this.handleOperation=this.handleOperation.bind(this)
    this.handleBreakLength=this.handleBreakLength.bind(this)
    this.handleSessionLength=this.handleSessionLength.bind(this)
    this.handleTimer=this.handleTimer.bind(this)
  };
handleReset(){
  clearInterval(this.myCountDown)
  this.setState({
      brkLength:5,
      sesnLength:25,
      timer:1500,
      timerState:'false',
      timerType:'Session',
  })
}
timeCountDown(){
  console.log(this.state.timerState)
  if(this.state.timerState){
    this.myCountDown=setInterval(()=>{
      if(this.state.timer>0){
       this.setState(prevState=>({
       timer:prevState.timer-1
     }))
      }else if(this.state.timer<=0){
        clearInterval(this.myCountDown)
        this.soundPlay()
        this.setState({
          timerType:'Break',
          timer:this.state.brkLength*60,
        },()=>this.breakCountDown())      
      }
    }
   ,1000)
  }else{
    clearInterval(this.myCountDown)
  }
}
soundPlay(){
const audio= new Audio("https://raw.githubusercontent.com/freeCodeCamp/cdn/master/build/testable-projects-fcc/audio/BeepSound.wav")
audio.play()
}
breakCountDown(){
 if(this.state.timerState){   
    this.myCountDown=setInterval(()=>{
  if(this.state.timer>0){
    this.setState({timer:this.state.timer-1})
  }else if(this.state.timer<=0){
    clearInterval(this.myCountDown)
    this.soundPlay()
    this.setState({
      timerType:'Session',
      timer:this.state.sesnLength*60
    })
  }
  
},1000)   
 }else{
   clearInterval(this.myCountDown)
 }

}
handleTimer(){
  console.log(this.state.timerState)
  if(this.state.timerType=="Session"){
  this.setState({
    timerState:!this.state.timerState
  },()=>this.timeCountDown())
  }else if(this.state.timerType=="Break"){
  this.setState({
    timerState:!this.state.timerState
  },()=>this.breakCountDown()) 
  }
}
handleOperation(stateToChange,amount){
const breakLength=this.state.brkLength 
const sessionLength=this.state.sesnLength
if(stateToChange=="sesnLength"&&sessionLength==1&&amount<0){
  return
}else if(stateToChange=="sesnLength"&&sessionLength==60&&amount>0){
  return
}else if(stateToChange=="sesnLength"){ 
  this.setState({
  [stateToChange]:this.state[stateToChange]+Number(amount)*1,
  timer:this.state.timer+Number(amount)*60
})
}
if(stateToChange=="brkLength"&&breakLength==1&&amount<0){
  return
}else if (stateToChange=="brkLength"){
this.setState({[stateToChange]:this.state[stateToChange]+Number(amount)})
}
}
handleBreakLength(e){
const {value}=e.currentTarget
const type="brkLength"
this.handleOperation(type,value)
}
handleSessionLength(e){
const {value}=e.currentTarget
const type="sesnLength"
this.handleOperation(type,value)
}
  render(){
    return(
      <div>
          <TimerLengthControl 
              title="Break Length"
              titleID="break-label"
              decrementID="break-decrement"
              incrementID="break-increment"
              spanID="break-length"
              span={this.state.brkLength}
              onClick={this.handleBreakLength}
            />
          <TimerLengthControl 
              title="Session Length"
              titleID="session-label"
              decrementID="session-decrement"
              incrementID="session-increment"
              spanID="session-length"
              span={this.state.sesnLength}
              onClick={this.handleSessionLength}
            />
          <TimerControl 
            timeLeft={this.state.timer}
            resetHandler={this.handleReset}
            timerHandler={this.handleTimer}
            timerType={this.state.timerType}
            
            />
      </div>
    );
  }
}


ReactDOM.render(<App />,document.getElementById("root"))
<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>
<!DOCTYPE html>
<html lang="en">
  <head>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
  </head>
  <body> 
    <div id="root">
    </div>
  </body>
</html>


Solution

  • The problem is that you've used 'false' instead of false for the "off" value for timerState:

    this.setState({
        brkLength: 5,
        sesnLength: 25,
        timer: 1500,
        timerState: 'false', // <==== here
        timerType: 'Session',
    });
    

    That's a string, not a boolean. And since it's a non-blank string, it's truthy. So

    if (this.state.timerState) {
    

    ...branches into the if block when timerState is 'false'

    Later, when you do

    this.setState({
        timerState: !this.state.timerState
    }, () => this.timeCountDown());
    

    ...it changes 'false' (a string) to false (a boolean).

    Booleans don't go in quotes:

    this.setState({
        brkLength: 5,
        sesnLength: 25,
        timer: 1500,
        timerState: false, // <====
        timerType: 'Session',
    });
    

    Updated:

    function formatTime(time) {
        let minutes = Math.floor(time / 60);
        let seconds = time % 60;
        minutes = minutes < 10 ? "0" + minutes : minutes;
        seconds = seconds < 10 ? "0" + seconds : seconds;
        return minutes + ":" + seconds;
    }
    
    
    const TimerLengthControl = (props) => (
        <div className="LengthContainer">
            <div className="controlTitle" id={props.titleID}>{props.title}</div>
            <div>
                <button
                    id={props.decrementID}
                    value="-1"
                    type={props.type}
                    onClick={props.onClick}
                >
                    <i className="fas fa-arrow-down"></i>
                </button>
                <span id={props.spanID}>{props.span}</span>
                <button
                    id={props.incrementID}
                    value="+1"
                    type={props.type}
                    onClick={props.onClick}
                >
                    <i className="fas fa-arrow-up"></i>
                </button>
            </div>
        </div>
    );
    
    const TimerControl = (props) => (
        <div className="timerControlContainer">
            <div className="timerContainer">
                <div id="timer-label">{props.timerType}</div>
                <div id="time-left">{formatTime(props.timeLeft)}</div>
            </div>
            <div className="buttonContainer">
                <button id="start_stop" onClick={props.timerHandler}>
                    <i className="fas fa-play" />
                    <i className="fas fa-pause" />
                </button>
                <button id="reset">
                    <i className="fas fa-sync" onClick={props.resetHandler} />
                </button>
            </div>
        </div>
    );
    
    class App extends React.Component {
        constructor() {
            super();
            this.state = {
                brkLength: 1,
                sesnLength: 1,
                timer: 60,
                timerState: false, // <==== here
                timerType: 'Session',
            };
            this.handleReset = this.handleReset.bind(this);
            this.handleOperation = this.handleOperation.bind(this);
            this.handleBreakLength = this.handleBreakLength.bind(this);
            this.handleSessionLength = this.handleSessionLength.bind(this);
            this.handleTimer = this.handleTimer.bind(this);
        };
        handleReset() {
            clearInterval(this.myCountDown);
            this.setState({
                brkLength: 5,
                sesnLength: 25,
                timer: 1500,
                timerState: false, // <==== here
                timerType: 'Session',
            });
        }
        timeCountDown() {
            console.log(1, this.state.timerState);
            if (this.state.timerState) {
                this.myCountDown = setInterval(() => {
                    if (this.state.timer > 0) {
                        this.setState(prevState => ({
                            timer: prevState.timer - 1
                        }));
                    } else if (this.state.timer <= 0) {
                        clearInterval(this.myCountDown);
                        this.soundPlay();
                        this.setState({
                            timerType: 'Break',
                            timer: this.state.brkLength * 60,
                        }, () => this.breakCountDown());
                    }
                }
                    , 1000);
            } else {
                clearInterval(this.myCountDown);
            }
        }
        soundPlay() {
            const audio = new Audio("https://raw.githubusercontent.com/freeCodeCamp/cdn/master/build/testable-projects-fcc/audio/BeepSound.wav");
            audio.play();
        }
        breakCountDown() {
            if (this.state.timerState) {
                this.myCountDown = setInterval(() => {
                    if (this.state.timer > 0) {
                        this.setState({ timer: this.state.timer - 1 });
                    } else if (this.state.timer <= 0) {
                        clearInterval(this.myCountDown);
                        this.soundPlay();
                        this.setState({
                            timerType: 'Session',
                            timer: this.state.sesnLength * 60
                        });
                    }
    
                }, 1000);
            } else {
                clearInterval(this.myCountDown);
            }
    
        }
        handleTimer() {
            console.log(2, this.state.timerState);
            if (this.state.timerType == "Session") {
                this.setState({
                    timerState: !this.state.timerState
                }, () => this.timeCountDown());
            } else if (this.state.timerType == "Break") {
                this.setState({
                    timerState: !this.state.timerState
                }, () => this.breakCountDown());
            }
        }
        handleOperation(stateToChange, amount) {
            const breakLength = this.state.brkLength;
            const sessionLength = this.state.sesnLength;
            if (stateToChange == "sesnLength" && sessionLength == 1 && amount < 0) {
                return;
            } else if (stateToChange == "sesnLength" && sessionLength == 60 && amount > 0) {
                return;
            } else if (stateToChange == "sesnLength") {
                this.setState({
                    [stateToChange]: this.state[stateToChange] + Number(amount) * 1,
                    timer: this.state.timer + Number(amount) * 60
                });
            }
            if (stateToChange == "brkLength" && breakLength == 1 && amount < 0) {
                return;
            } else if (stateToChange == "brkLength") {
                this.setState({ [stateToChange]: this.state[stateToChange] + Number(amount) });
            }
        }
        handleBreakLength(e) {
            const { value } = e.currentTarget;
            const type = "brkLength";
            this.handleOperation(type, value);
        }
        handleSessionLength(e) {
            const { value } = e.currentTarget;
            const type = "sesnLength";
            this.handleOperation(type, value);
        }
        render() {
            return (
                <div>
                    <TimerLengthControl
                        title="Break Length"
                        titleID="break-label"
                        decrementID="break-decrement"
                        incrementID="break-increment"
                        spanID="break-length"
                        span={this.state.brkLength}
                        onClick={this.handleBreakLength}
                    />
                    <TimerLengthControl
                        title="Session Length"
                        titleID="session-label"
                        decrementID="session-decrement"
                        incrementID="session-increment"
                        spanID="session-length"
                        span={this.state.sesnLength}
                        onClick={this.handleSessionLength}
                    />
                    <TimerControl
                        timeLeft={this.state.timer}
                        resetHandler={this.handleReset}
                        timerHandler={this.handleTimer}
                        timerType={this.state.timerType}
    
                    />
                </div>
            );
        }
    }
    
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <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>
    <!DOCTYPE html>
    <html lang="en">
      <head>
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
      </head>
      <body> 
        <div id="root">
        </div>
      </body>
    </html>


    Also, as I said in my comment, when updating state based on existing state, in general you want to use the callback form. So instead of:

    this.setState({
        timerState: !this.state.timerState
    }, () => this.timeCountDown());
    

    do

    this.setState(
        ({timerState}) => ({timerState: !timerState}),
        () => this.timeCountDown()
    );