當我們寫React應用的時候,知道在組件中何時使用state何時不使用state,是非常重要的。在這篇文章中,我將回顧我所認為的使用state的最佳實踐:
- 如果component沒有自己的數據,那么其他數據便不應該影響它的state。
- 用于描述組件的state盡可能簡單。
- 運算和條件判斷移動到render函數。
這些規則如果有特殊情況,應該在適當的時候違反。不過如果你能夠一直都遵循它們的話,你會發現你的component更容易解耦,測試更容易寫,而且整個應用的bug也很少。下面讓我們仔細看看這些規則:
1. 如果component沒有自己的數據,那么其他數據便不應該影響它的state
第一,可能是最重要的一點,component的state不應該依賴于props傳遞。當然props可能向子組件傳遞state,例如,在一個普通的input組件中,為了禁用input的文字輸入,我可能選擇一個disabled的prop。但是當我說'state'的時候,我是明確的指component的state屬性。所以,當state開始依賴于它的props的時候,你可能會發現這是一段不好的代碼。看看以下代碼片段:
import React from 'react';
class UserWidget extends React.Component {
// BAD: 通過props接收到的值設置this.state.fullName
constructor (props) {
this.state = {
fullName: `${props.firstName} ${props.lastName}`
};
}
render () {
var fullName = this.state.fullName;
var picture = this.props.picture;
return (
<div>
<img src={picture} />
<h2>{fullName}</h2>
</div>
);
}
}
以上代碼有什么問題?一開始可能不是很明顯,但是假如firstName或者lastName改變了,UserWidget組件的視圖將不會改變。構造函數在組件初始化渲染執行之后只會調用一次,因此fullName的值永遠是第一次渲染時候的值。React新手可能經常會犯這樣的錯誤,因為setState是更新組件視圖的最簡單且最明顯的方式。
你應該問問你自己,該組件是否擁有這些數據,內部的firstName和lastName創建了嗎?如果沒有,那么state不應該依賴便不應該依賴于這些數據。那么最好的避免這個問題的方式是什么呢?在render函數里面計算fullName的值。
render () {
var fullName = `${this.props.firstName} ${this.props.lastName}`;
// ...
}
把fullName移動到render函數里面之后,我們將不用再關心fullName的值是否更新了。當props改變的時候,React會運行一個鉤子函數--componentWillReceiveProps,然而我還是會考慮這種反模式,因為它不需要增加項目的復雜性。
當然,如果你在組件初始化之后不關心props,那么這條規則將不會適用。
當使用React.createClass代替extends React.Component時候,則用getInitialState代替constructor。
有時候,"state"將需要設置一些值,在flux模式中,可能是根控制器組件監聽不同的stores。
2. 用于描述組件的state盡可能簡單
你應該盡可能的簡單的去描述一個組件的狀態。在很多種情況下,這意味著用布爾值是更好的方式。
思考下面的例子,我們有一些組件,它們在state里面的class屬性是基于clicked和hovered事件改變的。(不管你信不信,我看到過很多這樣的例子)
import React from 'react';
var cx = React.addons.classSet;
class ArbitraryWidget extends React.Component {
constructor() {
this.state = {
classes: []
};
}
// BAD: 當鼠標滑過的時候,把'hover'push到this.state.classes
handleMouseOver() {
var classes = this.state.classes;
classes.push('hover');
this.setState({ classes: classes });
}
// BAD: 當鼠標離開的時候,從this.state.classes移除'hover'
handleMouseOut() {
var classes = this.state.classes;
var index = classes.indexOf('hover');
classes.splice(index, 1);
this.setState({ classes: classes });
}
// BAD: 被點擊的時候,在this.state.classes切換'active'
handleClick() {
var classes = this.state.classes;
var index = classes.indexOf('active');
if (index != -1) {
classes.splice(index, 1);
} else {
classes.push('active');
}
this.setState({ classes: classes });
}
render() {
var classes = this.state.classes;
return (
<div className={cx(classes)}
onClick={this.handleClick.bind(this)}
onMouseOver={this.handleMouseOver.bind(this)}
onMouseOut={this.handleMouseOut.bind(this)}
/>
)
}
}
這個組件可以運行,但是我持保留意見。它現在的state是一個存著字符串類型的數組,this.state.classes = ['active', 'hover']
,不僅代碼的可讀性很差,而且改變起來特別麻煩。假如有其他組件依賴于我的這個class的數組,那么查看這個數組是否包含hover
肯定比查看hover
的布爾值是什么的難度要大。我們需要重構這段代碼,用布爾值代表組件是否應該有這些class,例如isHovering === true
意味著我是否應該使用hover這個class。
import React from 'react';
var cx = React.addons.classSet;
class ArbitraryWidget extends React.Component {
constructor() {
this.state = {
isHovering: false,
isActive: false
};
}
// GOOD: 當鼠標滑過的時候,this.state.isHovering設置為true
handleMouseOver() {
this.setState({ isHovering: true });
}
// GOOD: 當鼠標離開的時候,this.state.isHovering設置為false
handleMouseOut() {
this.setState({ isHovering: false });
}
// GOOD: 被點擊的時候,改變this.state.active
handleClick() {
var active = !this.state.isActive;
this.setState({ isActive: active });
}
render() {
// use the classSet addon to concat an array of class names together
var classes = cx([
this.state.isHovering && 'hover',
this.state.isActive && 'active'
]);
return (
<div className={cx(classes)}
onClick={this.handleClick.bind(this)}
onMouseOver={this.handleMouseOver.bind(this)}
onMouseOut={this.handleMouseOut.bind(this)}
/>
);
}
}
為了使用這些state的布爾值,我們必須在render函數里面計算class數組。但是,我們增強了代碼的可讀性,this.state.isHovering
遠比this.state.classes.indexOf('hover') != -1
更能代表組件實際的狀態。這個組件更容易擴展和測試,因為我們不需要考慮數組的構建。
我想再說一遍,你應該始終以用最簡單的方式表示state為目標。這并不一定意味著你只能存儲布爾值,有可能是深層嵌套的對象,也可能是數字、字符串或者函數。
想象一下作為其他人,試圖觀察組件返回的一個class的數組的狀態,這個數組對于你是否有用呢?當然沒有。相比之下布爾值isActive
是更為可行的。我希望你明白我的意思。
3. 運算和條件判移動到render函數
在前面的兩條規則中,這一條其實已經提到了。然而,它仍然是值得注意的。盡可能的在render函數中進行最后一步運算。雖然這樣也許會略慢于其他方法,但它能確保最少的重定向組件,在輕微的性能提升之前,我們應該更注重代碼的可讀性和擴展性。
我需要連接prop中的firstName和lastName?把它移動到render函數。我的組件需要使用哪個class?在render函數中做決定。如果我的todo列表沒有任何項目,我應該顯示在text框中顯示一個placeholder?在render函數中做決定。我需要格式化電話號碼?在render函數中做決定。我該如何呈現出子組件?在render函數中做決定。我今天要吃午飯嗎?在render函數中做決定。
當然,你不要把所有代碼都放在一個函數里面。相反,最好把它們分割成合適的helper函數(用一個好的名字),關鍵是你用render函數做太多的事情的話,應該減少它的復雜性。你可以用一個前綴來表示helper函數。例如:
// GOOD: Helper function to render fullName
renderFullName () {
return `${this.props.firstName} ${this.props.lastName}`;
}
render () {
var fullName = this.renderFullName();
// ...
}
CPU密集運算
因為我建議你把所有的東西都推遲到render函數中,它會導致CPU密集運算也會推遲。為了避免重復復雜的渲染,考慮memoization的功能。
不要把變量存儲到component實例上
不要像下面這樣做:
class ArbitraryWidget extends React.Component {
constructor () {
this.derp = 'something';
}
handleClick () {
this.derp = 'somethingElse';
}
render () {
var something = this.derp;
}
}
這是非常不好的,不僅是因為你沒有遵守用this.state
存儲值的約定,而且this.derp
改變的時候,不會自動觸發render。