reactjs中props和state最佳實踐
props & state的誤用產生的bug,花費了我們大量的修復時間。在一個app中如果包含有成百不可見的危險代碼,對我們的應用將是非常危險和頭痛的事情。
這篇文章中,你將明白這些不可預知的行爲產生的原因,這樣在編程中出錯將變得非常困難。這也可以用來解決我們其他JS編程中遇到的類似的問題。
在我開始學習reactjs時,第一個困惑我的事情是在使用props傳遞數據給組件、在組件的state中怎樣讀取數據源和store中提供的數據。這一方面,我低估了問題的難度。
一個簡單的例子
這一節中我們將明白它們(props & state)爲什麼這麼容易混淆。
下面這段代碼是reactjs langding page中的第一個例子修改而來,其中的HelloMessage組件,在這裏我們使用Counter來代替。
/* Snippet 1 */
var Counter = React.createClass({
render: function() {
return <span>Count is {this.props.count}</span>;
}
});
React.render(<Counter count={5} />, mountNode);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
非常容易,對不對?但是,我們現在使用state來做相同的事情。
/* Snippet 2 */
var items = ['item1', 'item2', 'item3'];
var Counter = React.createClass({
getInitialState: function() {
return {count: items.length};
},
render: function() {
return <span>Count is {this.state.count}</span>;
}
});
React.render(<Counter />, mountNode);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
哪一個是惡魔?一旦我們程序變大,哪一個將是不可見的錯誤?實際上,兩種用法都有可能正確,也有可能錯誤,這取決於它們使用時的上下文環境。接下來,我將嘗試使用最好的方法來解釋這是爲什麼。
我相信這些困惑的出現都是因爲理解過於簡單造成的。我們不能責備reactjs上面的例子不夠好,這些例子確實是一個很好的入門實例。但是沒有任何上下文環境,這樣在我們使用的時候就會遇到困難。第一件比較明顯的事情就是要告訴我們這個Counter組件的目的。我們是要用它來展示一個帖子的數量呢?還是要顯示類似於facebook導航條上的消息數?
TweetList Component
這裏我們將建立一個帖子列表組件(TweetList)和一個相對應的帖子計數器組件(Counter),來說明我們怎樣及什麼時候來使用props & state。以此說明當Counter展示一個帖子數量時該怎麼使用。當我們傳入1時,顯示的是one,以此類推。首先我們嘗試在沒有props & state的幫助下來建立組件。
/* Snippet 3 */
var _tweet = {
author: 'John',
content: 'My first ReactJS tweet',
impressions: 5
};
var Counter = React.createClass({
render: function() {
var wordsDataSet = ['zero', 'one', 'two', 'three', 'four', 'five'];
var word = wordsDataSet[_tweet.impressions];
return (
<span>Impressions: {word}</span>
);
}
});
var TweetItem = React.createClass({
render: function() {
return (
<div>
<div>@{_tweet.author} says:</div>
<div>{_tweet.content}</div>
<Counter />
</div>
);
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
這一小段代碼非常易於理解,當然你也可能發現了一下可怕的缺陷。最糟糕的是,這個Counter組件只能用於一個帖子,而不能複用。爲了改變這一特徵,我們來看看我們需要爲Counter組件定義哪些屬性:
1)不能將count定義死。(它應該是可以被用於tweet impressions, retweets, number of followers等等)
2)我們可以不用關心是怎樣獲取的數據。(它可以來自於JS對象、HTML屬性、一個ajax回調等等)
3)它應該是很容易被任何層級的複雜組件調用。
Flux框架要求我們,組件層次中的最外層的那個組件(controller-view 組件)應該處理一個state,然後通過props傳遞給它的子組件們。這樣無論何時何地要改變view將變得容易,無論何時只要controller-view的state發生變化,它都將觸發它的render函數,子組件們將會獲取新的props,然後逐級影響並更新其他組件。通過這些特性以及上面我們總結的Counter組件屬性,Counter組件不應定義任何state,而是隻能依靠props。所以我們修改了我們的代碼:
/* Snippet 4 */
// ...
var Counter = React.createClass({
render: function() {
var wordsDataSet = ['zero', 'one', 'two', 'three', 'four', 'five'];
var word = wordsDataSet[this.props.count];
return (
<span>{word}</span>
);
}
});
var TweetItem = React.createClass({
getInitialState: function() {
return {tweet: _tweet};
},
render: function() {
return (
<div>
<div>@{_tweet.author} says:</div>
<div>{_tweet.content}</div>
Impressions:
<Counter count={_tweet.impressions} />
</div>
);
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
現在我們的Counter變得更好了。當TweetItem是最外層組件(controller-view)時,那麼它就應該有一個state。但是我們一直還有一個問題沒有解決,TweetItem不太可能單獨使用,我們也要將它放在tweet列表中,所以它也不應該擁有state。接下來,讓我們來修改TweetItem,並且創建TweetList組件。
/* Snippet 5 */
// ...
var TweetItem = React.createClass({
render: function() {
return (
<div>
<div>@{this.props.tweet.author} says:</div>
<div>{this.props.tweet.content}</div>
Impressions:
<Counter count={this.props.tweet.impressions} />
</div>
);
}
});
var TweetList = React.createClass({
getInitialState: function() {
return {tweets: []};
},
componentDidMount: function() {
var self = this;
$.get('/latest-tweets.json', function(_tweets) {
self.setState({tweets: _tweets});
});
},
render: function() {
var listItems = this.state.tweets.map(function(tweet, index) {
return (
<li key={index}>
<TweetItem tweet={tweet} />
</li>
);
});
return (
<ul>
{listItems}
</ul>
);
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
現在代碼看起來更好了。最外層組件TweetList擁有了該應用的state。在componentDidMount函數中我們通過jQuery ajax來獲取最後一個tweet,並且更新了其state。
提示:儘管講ajax代碼寫在TweetList組件中沒有任何問題,但是最好的做法還是將其邏輯分離。比如寫在stores中。這樣看起來更簡潔明瞭。
UnreadMessagesCount Component
還記得我開始時候說過,不同版本的Counter組件的正確還是錯誤主要取決於其使用它的上下文環境嗎?之前我們創建了一個只依賴於props的Counter組件,接下來,我們將創建一個依賴於state的Counter組件。
想象一下我們使用messages來代替tweets來工作,而且我們應該有一個MessageList組件。我們想讓它最大限度的像TweetList組件一樣渲染,只不過沒有impressions數量。我們來一起定義這個組件的特徵:
1)我們只關心unread messages的數量。在整個APP中我們只存儲了一個數字。
2)只要unread messages一創建,它將自動更新。
3)無論在哪裏它應該都可以被獨立調用。類似於Facebook頁眉中可以被使用。即我們的MessageList和Counter組件需要保持彼此分離。
我們的HTML文件裏的代碼應該看起來像這樣:
<body>
<header>
<span id="counter-mount-node"></span>
</header>
<main>
<span id="message-list-mount-node"></span>
</main>
</body>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
你是否已經看出來了,Counter組件自己就是一個controller-view。而且我們也不需要對它進行重用,它的代碼簡單明瞭。Bat-Signal規定我們只能使用state來實現。
var UnreadMessagesCounter = React.createClass({
getInitialState: function() {
return {count: 0};
},
componentDidMount: function() {
messageStore.addChangeListener(this.onMessagesChanged);
},
onMessagesChanged: function() {
var count = messageStore.getUnreadMessagesCount();
this.setState({count: count});
},
render: function() {
return (
<span>You have {this.state.count} unread messages</span>
);
}
});
var mountNode = document.getElementById("counter-mount-node");
React.render(<UnreadMessagesCounter />, mountNode);
$(document).ready(function() {
messageStore.loadMessagesAndEmitChange();
setInterval(messageStore.loadMessagesAndEmitChange, 5000);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
希望這些代碼比較容易理解。這個組件非常的直接。我們沒有使用邏輯代碼,因爲還沒有messageStore。我們來看看這個相應的store是怎樣的:
1)它應該只有一個通道來檢索信息列表;
2)讓組件訂閱任何更新時事件,因此它們可以得到最後一個list。這裏可以通過註冊一個addChangeListener的回調方法來加以實現;
3)loadMessagesAndEmitChange方法在頁面加載後,每個5秒被調用一次。
如果MessageList可以通過messageStore來監聽其變化。我們就不用像TweetList那樣通過MessageList組件代碼來獲取更新。這裏我們省略了store代碼。
結論
到目前爲止,我們學會了關於reactjs的使用方法。我會讓它更有趣的展示一個Twitter克隆在後面的階段,或者是更令人興奮的東西,我會做出決定在一天或兩天。即將到來的文章將介紹其他主題,將最後的基礎教程。如果你正在尋找問題運行片段在評論中讓我知道,我很樂意幫助你。