幾個月前,我開始停止使用React的 setState 。我並不是不再需要元件狀態,而且不再用React來管理我的元件狀態。
setState對於新手來說不是很友好,即使是有經驗的React程式設計師在使用setState時,也很容易出bug,比如:
Bug產生的原因是忘記了React的state是非同步的;從日誌列印延遲可以看出來。
React的官方文件已將把使用setState可能會出現的所有問題都總結了:
注意:
永遠不要直接修改
this.state
,需要通過呼叫this.setState
方法來替換你修改後的值。把this.state
當做不可變資料來處理。
setState()
不會馬上去改變this.state
,而是會排隊等待處理,所以當你呼叫setState()
後訪問this.state
,有可能會返回舊的state
。當你呼叫
setState()
時,無法保證是同步執行的,因為為了保證效能可能會被批處理。
setState()
總是會觸發render()
進行重新渲染,除非在shouldComponentUpdata()
控制了渲染邏輯。如果用了可變資料結構以及在shouldComponentUpdata()
中並沒有控制渲染邏輯,呼叫setState()
將不會觸發重新渲染渲染。
總的來說,使用setState
會帶來三個問題:
1. setState
是非同步的
許多開發人員起初並沒有意識到這一點,當你設定了新的state
,卻發現沒有變化,這是setState
最詭異的地方,因為setState
呼叫的時候看起來並不是非同步。比如下面的程式碼:
class Select extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
selection: props.values[0]
};
}
render() {
return (
<ul onKeyDown={this.onKeyDown} tabIndex={0}>
{this.props.values.map(value =>
<li
className={value === this.state.selection ? 'selected' : ''}
key={value}
onClick={() => this.onSelect(value)}
>
{value}
</li>
)}
</ul>
)
}
onSelect(value) {
this.setState({
selection: value
})
this.fireOnSelect()
}
onKeyDown = (e) => {
const {values} = this.props
const idx = values.indexOf(this.state.selection)
if (e.keyCode === 38 && idx > 0) { /* up */
this.setState({
selection: values[idx - 1]
})
} else if (e.keyCode === 40 && idx < values.length -1) { /* down */
this.setState({
selection: values[idx + 1]
})
}
this.fireOnSelect()
}
fireOnSelect() {
if (typeof this.props.onSelect === "function")
this.props.onSelect(this.state.selection) /* not what you expected..*/
}
}
ReactDOM.render(
<Select
values={["State.", "Should.", "Be.", "Synchronous."]}
onSelect={value => console.log(value)}
/>,
document.getElementById("app")
)複製程式碼
乍一看沒什麼問題,但是這個select
元件有一個bug,上面的git圖已經很好的證明了。onSelect()
方法觸發時總是得到前一個state.selection
的值,因為setState
還沒有完成,fireOnSelect
就被呼叫了。我認為應該把setState
重新命名為scheduleState
或者要求傳入回撥。
這個bug很容易修復,棘手的地方在於你很難發現它。
2. setState
引起沒有必要的渲染
setState
的第二個問題在於它總是會觸發重新渲染,很多時候這種渲染是沒有必要的。你可以通過printWasted(React提供的效能工具)來檢測它會在什麼時候重新渲染。從三個方面來粗略的講為什麼重新渲染有時候是沒有必要的:
當新的
state
和舊的state
是一樣的。可以通過shouldComponentUpdate
來解決,也可以用純渲染庫來解決。只有某些時候
state
的改變才和渲染有關係。第三,某些時候
state
和檢視層一點關係都沒有,比如用來管理事件的監聽器,定時器等相關的state
。
3. setState
不可能管理所有元件的狀態
接著上面最後那點說,不是所有的元件狀態都需要通過setState
來儲存和更新。大多數複雜的元件通常需要管理定時器迴圈,介面請求,事件等等。如果用setState
來管理,不僅僅會引起沒有必要渲染,而且會造成死迴圈。
用MobX來管理元件狀態
程式碼如下:
import {observable} from "mobx"
import {observer} from "mobx-react"
@observer class Select extends React.Component {
@observable selection = null; /* MobX managed instance state */
constructor(props, context) {
super(props, context)
this.selection = props.values[0]
}
render() {
return (
<ul onKeyDown={this.onKeyDown} tabIndex={0}>
{this.props.values.map(value =>
<li
className={value === this.selection ? 'selected' : ''}
key={value}
onClick={() => this.onSelect(value)}
>
{value}
</li>
)}
</ul>
)
}
onSelect(value) {
this.selection = value
this.fireOnSelect()
}
onKeyDown = (e) => {
const {values} = this.props
const idx = values.indexOf(this.selection)
if (e.keyCode === 38 && idx > 0) { /* up */
this.selection = values[idx - 1]
} else if (e.keyCode === 40 && idx < values.length -1) { /* down */
this.selection = values[idx + 1]
}
this.fireOnSelect()
}
fireOnSelect() {
if (typeof this.props.onSelect === "function")
this.props.onSelect(this.selection) /* solved! */
}
}
ReactDOM.render(
<Select
values={["State.", "Should.", "Be.", "Synchronous."]}
onSelect={value => console.log(value)}
/>,
document.getElementById("app")
)複製程式碼
效果如下:
用同步的元件狀態的機制沒有出現意想不到的bug。
上面的程式碼片段不僅簡潔美觀,MobX
還解決了setState
的全部問題:
改變state
馬上就反映到了元件state
上。這讓程式碼邏輯和程式碼重用變得更簡單。你不用去擔心state
是否更新了。
MobX
在執行時候確定哪些state
與檢視層關聯,暫時與檢視層無關的State
不會引起重新渲染,直到再次關聯。
所以可渲染和不可渲染狀態是統一處理的。
此外,直接修改state
物件的低階錯誤不能再犯了。還有,不要擔心是否還能用shouldComponentUpdate 和或者PureRenderMixin方法,MobX
也很好的處理了。最後,你也許會想,我如何等到setState
處理完成呢,你可以使用compentDidUpdate
生命週期。
最後:
我已經停止使用React來管理元件狀態了,而是用MobX
代替。現在React
成了“正真”的檢視層:)。
以下是實現的Dome: