在 Reactjs 有没有 setState()的同步替代方案

根据 医生的解释:

SetState ()不会立即变异 this.state,而是创建一个挂起的状态转换。在调用此方法后访问 this.state 可能会返回现有值。

不能保证调用 setState 的同步操作,为了提高性能,可能会对调用进行批处理。

因此,由于 setState()是异步的,并且不能保证它的同步性能。是否有 setState()的替代方案是同步的。

比如说

//initial value of cnt:0
this.setState({cnt:this.state.cnt+1})
alert(this.state.cnt);    //alert value:0

由于 alert值是以前的值,那么使用 setState()给出 alert value:1的替代方案是什么呢。

关于堆栈溢出的问题很少有类似于这个问题的,但是没有一个地方我能找到正确的答案。

107092 次浏览

No, there is not. React will update the state when it sees fit, doing things such as batching setState calls together for efficiency. It may interest you that you are able to pass a function into setState instead, which takes the previous state, so you may choose your new state with good knowledge of the previous one.

Short answer to your question is - NO, react doesn't have sync method setState.

As you have read from the documentation, there is NO sync alternative, reason as described is performance gains.

However I presume you want to perform an action after you have changed your state, you can achieve this via:

class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 1
};
    

console.log('initial state', this.state);
}
  

updateState = () => {
console.log('changing state');
this.setState({
x: 2
},() => { console.log('new state', this.state); })
}
  

render() {
return (
<div>
<button onClick={this.updateState}>Change state</button>
</div>
);
   

}
}


ReactDOM.render(
<MyComponent />,
document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

I was able to trick React into calling setState synchronously by wrapping my code in setTimeout(() => {......this.setState({ ... });....}, 0);. Since setTimeout puts stuff at the end of the JavaScript event queue, I think React detects the setState is within it and knows it can't rely on a batched setState call (which would get added to the end of the queue).

If this is required I would suggest using a callback in your setState function (and I also suggest using a functional setState).

The callback will be called after the state has been updated.

For example, your example would be

//initial value of cnt:0
this.setState(
(state) => ({cnt: state.cnt+1}),
() => { alert(this.state.cnt)}
)

as per documentation here : https://facebook.github.io/react/docs/react-component.html#setstate

Note: Official docs say, "Generally we recommend using componentDidUpdate() for such logic instead."

Yes, there is a method with which we can make our synchronous setState. But its performance maybe not good as normally For example, we have

class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: 0
};
}


changeState(){
console.log('in change state',this.state.data);
this.state.data = 'new value here'
this.setState({});
console.log('in change state after state change',this.state.data);
}


render() {
return (
<div>
<p>{this.state.data}</p>
<a onClick={this.changeState}>Change state</a>
</div>
);


}
}

In this example, we change the state first and then render our component.

You could wrap setState in a function returning a promise, and then use this function with the await keyword to cause your code to wait until the state has been applied.

Personally, I would never do this in real code, instead I would just put the code I wish to execute after the state update in the setState callback.

Nerveless, here is an example.

class MyComponent extends React.Component {


function setStateSynchronous(stateUpdate) {
return new Promise(resolve => {
this.setState(stateUpdate, () => resolve());
});
}


async function foo() {
// state.count has value of 0
await setStateSynchronous(state => ({count: state.count+1}));
// execution will only resume here once state has been applied
console.log(this.state.count);  // output will be 1
}
}

In the foo function, the await keyword causes the code execution to pause until the promise returned by setStateSynchronous has been resolved, which only happens once the callback passed to setState is called, which only happens when the state has been applied. So execution only reaches the console.log call once the state update has been applied.

docs for async/await:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

It may sound weird but yes setState can work synchronously in react. How so? This is POC which I've created to demonstrate it.

Pasting the only app JS code.

Maybe it's possible that I'm missing something but this was actually happening in my application that's when I came to know about this effect.

Correct me if this kind of behavior is expected in React which I'm unaware of. When there are multiple setState on main thread the setState runs a Batch combining all the setState on the main method. Whereas the Scenario is different when the same things go inside the async Function.

import React, { Component } from 'react';
import './App.css';


class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
this.asyncMethod = this.asyncMethod.bind(this);
this.syncMethod = this.syncMethod.bind(this);
}


asyncMethod() {
console.log("*************************")
console.log("This is a async Method ..!!")
this.setState({
counter: this.state.counter + 1
}, () => {
console.log("This is a async Method callback of setState. value of counter is---", this.state.counter);
})
console.log("This is a async Method on main thread. value of counter is---", this.state.counter);
console.log("*************************")
}


syncMethod() {
var that = this;
console.log("*************************")
console.log("This is a sync Method ..!!")
that.setState({counter: "This value will never be seen or printed and render will not be called"});
that.setState({counter: "This is the value which will be seen in render and render will be called"});
setTimeout(() => {
that.setState({counter: "This is part is synchronous. Inside the async function after this render will be called"});
console.log("setTimeout setState");
that.setState({counter: "This is part is aslso synchronous. Inside the async function after this render will be called"});
}, 10)
console.log("This is a sync Method on Main thread. value of counter is---", this.state.counter);
console.log("*************************")
}


render() {
console.log("Render..!!",this.state.counter);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
<button onClick={this.asyncMethod}>AsyncMethod</button>
<button onClick={this.syncMethod}>SyncMethod</button>
</div>
);
}
}


export default App;

Use React Hooks instead:

function MyComponent() {
const [cnt, setCnt] = useState(0)


const updateState = () => {
setCnt(cnt + 1)
}


useEffect(() => {
console.log('new state', cnt)
}, [cnt])


return (
<div>
<button onClick={updateState}>Change state</button>
</div>
)
}

In functional components I do this:

const handleUpdateCountry(newCountry) {
setIsFetching(() => true);
setCompanyLocation(() => newCountry);
setIsFetching(() => false);
}

Correct me if I'm wrong but as far as I know this is synchronous then and it also just worked in my situation.

In some cases, an alternative is using refs (createRef or useRef) instead of states.

//initial value of cnt:0
const cnt = React.createRef(0); //or React.useRef(0);
cnt.current++;
alert(cnt.current); //output: 1

So far the best solution for me is to use the callback function.

this.setState({cnt:this.state.cnt+1},() =>{
// other task which we want to run synchronously
alert(this.state.cnt);
})

according to the official documentation of react, we can make setState synchronous by passing the callback function in the second argument.

You can use flushSync from ReactDOM to update the state synchronously as suggested by react docs.

https://reactjs.org/docs/react-component.html