翻译文章:ReactJS Props vs State Best Practices
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 Count is {this.props.count};
}
});
React.render(5} />, mountNode);
非常容易,对不对?但是,我们现在使用state来做相同的事情。
/* Snippet 2 */
var items = ['item1', 'item2', 'item3'];
var Counter = React.createClass({
getInitialState: function() {
return {count: items.length};
},
render: function() {
return Count is {this.state.count};
}
});
React.render( , mountNode);
哪一个是恶魔?一旦我们程序变大,哪一个将是不可见的错误?实际上,两种用法都有可能正确,也有可能错误,这取决于它们使用时的上下文环境。接下来,我将尝试使用最好的方法来解释这是为什么。
我相信这些困惑的出现都是因为理解过于简单造成的。我们不能责备reactjs上面的例子不够好,这些例子确实是一个很好的入门实例。但是没有任何上下文环境,这样在我们使用的时候就会遇到困难。第一件比较明显的事情就是要告诉我们这个Counter组件的目的。我们是要用它来展示一个帖子的数量呢?还是要显示类似于facebook导航条上的消息数?
这里我们将建立一个帖子列表组件(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 (
Impressions: {word}
);
}
});
var TweetItem = React.createClass({
render: function() {
return (
@{_tweet.author} says:
{_tweet.content}
);
}
});
这一小段代码非常易于理解,当然你也可能发现了一下可怕的缺陷。最糟糕的是,这个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 (
{word}
);
}
});
var TweetItem = React.createClass({
getInitialState: function() {
return {tweet: _tweet};
},
render: function() {
return (
@{_tweet.author} says:
{_tweet.content}
Impressions:
);
}
});
现在我们的Counter变得更好了。当TweetItem是最外层组件(controller-view)时,那么它就应该有一个state。但是我们一直还有一个问题没有解决,TweetItem不太可能单独使用,我们也要将它放在tweet列表中,所以它也不应该拥有state。接下来,让我们来修改TweetItem,并且创建TweetList组件。
/* Snippet 5 */
// ...
var TweetItem = React.createClass({
render: function() {
return (
@{this.props.tweet.author} says:
{this.props.tweet.content}
Impressions:
this.props.tweet.impressions} />
);
}
});
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 (
);
});
return (
{listItems}
);
}
});
现在代码看起来更好了。最外层组件TweetList拥有了该应用的state。在componentDidMount函数中我们通过jQuery ajax来获取最后一个tweet,并且更新了其state。
提示:尽管讲ajax代码写在TweetList组件中没有任何问题,但是最好的做法还是将其逻辑分离。比如写在stores中。这样看起来更简洁明了。
还记得我开始时候说过,不同版本的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>
你是否已经看出来了,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 (
You have {this.state.count} unread messages
);
}
});
var mountNode = document.getElementById("counter-mount-node");
React.render( , mountNode);
$(document).ready(function() {
messageStore.loadMessagesAndEmitChange();
setInterval(messageStore.loadMessagesAndEmitChange, 5000);
});
希望这些代码比较容易理解。这个组件非常的直接。我们没有使用逻辑代码,因为还没有messageStore。我们来看看这个相应的store是怎样的:
1)它应该只有一个通道来检索信息列表;
2)让组件订阅任何更新时事件,因此它们可以得到最后一个list。这里可以通过注册一个addChangeListener的回调方法来加以实现;
3)loadMessagesAndEmitChange方法在页面加载后,每个5秒被调用一次。
如果MessageList可以通过messageStore来监听其变化。我们就不用像TweetList那样通过MessageList组件代码来获取更新。这里我们省略了store代码。
到目前为止,我们学会了关于reactjs的使用方法。我会让它更有趣的展示一个Twitter克隆在后面的阶段,或者是更令人兴奋的东西,我会做出决定在一天或两天。即将到来的文章将介绍其他主题,将最后的基础教程。如果你正在寻找问题运行片段在评论中让我知道,我很乐意帮助你。