【问题标题】:SetState callback gets called before state is mutated in react在状态发生突变之前调用 SetState 回调
【发布时间】:2021-06-15 16:03:43
【问题描述】:

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

我定义了一个类组件来存储状态数据,"timerState",这是本例的主要焦点,将在真假之间切换

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

handleTimer 函数将在 onclick 事件发生后启动。由于 setstate 是异步运行的,我不希望在状态突变之前调用函数-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,它们都打印“ 错误”。

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)
  }
}

我想知道上面的代码 sn-p 出了什么问题。 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({timerState: !this.state.timerState}, /*...*/);) 更新状态时,使用setState 的回调形式几乎总是更可靠:this.setState(({timerState}) =&gt; ({timerState: !timerState}), /*...*/);。这是因为状态更新的方式可以是batched together。这可能很好地解释了您的问题,但我需要一个可运行的示例来查看。
  • (为什么不只看链接的?因为 SO 的工作方式,你的整个问题 [包括任何必要的代码] 必须 in 你的问题,而不仅仅是链接. 三个原因:人们不应该去场外帮助您;某些网站对某些用户被屏蔽;链接失效,使问题及其答案对未来的人们毫无用处。)
  • @T.J.克劳德感谢您的回复。我只是通过 stack sn-p 包含了上面的所有代码。现在是一个带有最小可重现示例的问题吗?我对这个社区很陌生,有时我忘记了应该严格遵守的原则。
  • 是的,太好了!虽然它不是真的最小,但似乎有很多你不需要证明问题的地方。

标签: javascript html reactjs setstate


【解决方案1】:

这种方法对您有帮助吗?

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

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

【讨论】:

  • 我刚刚更改了我的代码并按照您的指导进行了尝试,但不知何故它无法按预期工作。在我第一次点击时,console.log 显示的结果仍然是“错误的”。 this.setState(prevState=&gt;({ timerState:!prevState.timerState }),()=&gt;this.timeCountDown())
【解决方案2】:

问题是您使用'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()
);

【讨论】:

    猜你喜欢
    • 2017-09-16
    • 2019-08-04
    • 1970-01-01
    • 2019-08-26
    • 1970-01-01
    • 2021-10-25
    • 2019-09-25
    • 2017-11-04
    • 2021-04-19
    相关资源
    最近更新 更多