John Huang
John Huang

Reputation: 348

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>

Upvotes: 1

Views: 1865

Answers (2)

T.J. Crowder
T.J. Crowder

Reputation: 1074048

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

Upvotes: 1

Tushar Shahi
Tushar Shahi

Reputation: 20421

Does this approach help you?

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

You want to use the current state value in your further updates. The most reliable way is to pass a function. This will always have the right value.

Upvotes: 2

Related Questions