在反应中状态发生变化之前调用 SetState 回调

SetState callback gets called before state is mutated in react

以下是导致我 运行 遇到麻烦的部分的代码分解。

我定义了一个 class 组件来存储状态数据和 "timerState" ,这是本例中的主要焦点,将在 true 和假

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

handleTimer 函数将在 onclick 事件发生后启动。由于 setstate 运行s 是异步的,我不想在状态突变之前调用函数-timeCountDownbreakCountDown,我设置它们作为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()) 
  }
}

然而,正如 console.log 显示在两个地方 - 一个在 handleTimer 中,另一个在 timeCountDown,它们都打印“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)
  }
}

我想知道上面的代码片段出了什么问题。 link 如果您想查看整个编码,请点击此处。

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>

这个方法对你有帮助吗?

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

您想在以后的更新中使用当前状态值。最可靠的方法是传递一个函数。这将始终具有正确的值。

问题是您使用 'false' 而不是 false 作为 timerState 的“关闭”值:

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

那是 字符串 ,不是布尔值。因为它是一个非空字符串,所以它是真实的。所以

if (this.state.timerState) {

...当 timerState'false'

时分支到 if

稍后,当你这样做时

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

...它将 'false'(字符串)更改为 false(布尔值)。

布尔值不放在引号中:

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

已更新:

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>


此外,正如我在评论中所说,当基于现有状态更新状态时,通常您希望使用回调形式。所以代替:

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

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