原文链接 https://facebook.github.io/react/docs/thinking-in-react.html
文章是以第一人称写的,所以少部分内容读起来比较奇怪,但是结构很清晰,适合初学者理解。翻译水平不够,欢迎大佬指导(可以对照原文和原文中的代码)。
正文开始:
React是我们构建性能良好的大型webapp的首选,它目前在我们的facebook和instagram中变现非常好。
在使用react的过程中,核心的一点是如何在它的组件化理念下去思考并构建自己的项目。在这篇文章中,我们会向你展示如何使用react的思考方式去构建一个产品搜索表格的小项目。
假设我们现在已经有了JSON API和初步的设计稿,如下所示:
JSON API返回的数据:
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
STEP1:按照UI设计稿层次化拆分出react组件
在设计稿上尝试使用方框去画出每一个组件以及它的子组件,然后加上自己可理解的命名。如果和UI沟通方便的话,可以查看一下他们PS软件中的设计稿,因为设计稿一般都是按图层和图层组去管理的,并且已经命名好了,所以可以借鉴很多东西,加快自己的工作流程。
如何去确定一个组件不能再拆分,你可以这样类比去考虑,在js编程的过程中是不是要判断是否要增加一个函数或对象。通俗一点来说,这种方式叫做单一功能原则,就是一个组件只负责一个功能,如果一个组件包含多个功能,就需要把它拆分为多个子组件。
还有另外一个技巧在拆分组件是可以借鉴,日常工作中,你会发现json数据是和UI(看以看成组件)一一对应的,这是因为json数据和UI都是根据相同的,需要展示的信息去设计、演化的,这就意味着你只需要拆分组件到每个组件只代表json中一块数据或一个字段,就可以完成拆分组件的工作。
你会看到这个app中有以下五个组件,我们已经用斜体注明了每个组件代表的数据。
1.FilterableProductTable(橙色):包裹了我们这个app
2.SearchBar(蓝色):接收用户的输入数据
3.ProductTable (绿色):根据用户的输入过滤显示数据
4.ProductCategoryRow (蓝绿色/青色):每个类目的标题
5.ProductRow (红色): 用一行代表每个产品
如果观察一下ProductTable这个组件,你会看到表格头部内name和label标签并没有单独划出组件,这时可以根据自己的偏好决定,如果两种方式犹豫不定的话。在这个例子中,我们把表头留作ProductTable的一部分是因为它的作用和ProductTable的作用是一样的,都是用来渲染产品列表的。另一种情况,如果表头变得更复杂,比如增加了价格区间,这时就非常必要增加一个ProductTableHeader组件了。
现在我们已经理清了需要的组件,接下来按层次组织一下所有组件,把有父组件的子组件作为子节点显示在层次列表中,如下:
FilterableProductTable
SearchBar
-
ProductTable
ProductCategoryRow
-
ProductRow
STEP2:使用react构建项目的静态版本
代码如下:
class ProductCategoryRow extends React.Component {
render() {
return <tr><th colSpan="2">{this.props.category}</th></tr>;
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(product) {
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
render() {
return (
<div>
<SearchBar />
<ProductTable products={this.props.products} />
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
现在有了app的组件和相关结构,可以开始构建工作了。这时最简单的方法是先直接使用已有数据去渲染UI,暂时不考虑app的动态数据是怎么交互和工作的。因为构建静态的版本只需要努力的敲大量代码而不用耗费脑力去思考数据怎么交互,反之,添加数据交互功能更费脑力。接下来,会解释原因。
在只需要渲染已有数据的情况下,你只要把这些数据作为组件的props传入即可。props是父组件向子组件传递数据的方式。如果你熟悉state的相关概念和用法,但是在构建静态版本这个阶段,牢记先不要去使用它。因为state适用于数据交互的,也就是说,数据在不断变化,所以这时先忘记state这个东西。
在具体的操作方式上,可以采用自上而下或自下而上的方式,也就是说,你可以选择从组件结构最上层(FilterableProductTable)或者从结构最下层(ProductRow)。在一些简单的项目中,通常选择从上之下的方法,而在更大的项目中,使用自下而上的方式更容易一些,并且可以同时写测试。
这一步最后,你已经有了可复用的组件去渲染已有数据,静态app版本中只有render()方法。组件结构最上层FilterableProductTable将已有数据作为自己的prop属性,如果你对数据做一点更改,并再次调用render(),UI就会再次更新。在现在app不复杂的情况下,观察UI如何更新,哪里需要更改是很清楚的,react的单向数据流或单向绑定会保证app的模块化和性能。
STEP-3: 确认组件的state(数量最少但能包含全部状态)
为了增强app的交互性,需要能够触发数据的变化,在react中提供了state使整个流程变得简单。
正确构建app首要思考的是找出app中所需要的最少量的state。原则就是不要做重复工作,挖掘最少的state,其他工作交给相关命令。举个栗子,现在要做一个TODOList,只要分理出一个事项列表的state就可以包括所有数据,而不必为每一个事项都提供一个state。在渲染每个事项的时候,使用包含数组的state就行了。
回到我们的app中,观察所有需要展示的数据,可以得出以下几个:
- 原始的产品列表
- 用户输入的搜索关键词
- 单选框的值
- 经过过滤展示的产品列表
让我们仔细查看每一个,确定哪一个是state。这里要考虑三个关于这些数据的问题:
- 这个数据是父组件的props属性传过来的吗?如果是,那它就不能作为state
- 这个数据一直保持不变吗?如果是,那它就不能作为state
- 这个数据可以用组件内其它的state或者是props计算而来吗?如果是,那它就不能作为state
原始的产品列表是使用props传入的,所以它不是state。用户输入的关键词和单选框的值是在时刻变化的,并且不能计算而来,看起来可以作为state。最后,过滤后的产品列表可以通过原始列表等以上三项数据计算而来,所以它不是state。
最后,app中的state是:
1.用户输入的搜索关键词
2.单选框的值
STEP4:确定state放置的具体组件和位置
代码:
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
return;
}
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
render() {
return (
<form>
<input type="text" placeholder="Search..." value={this.props.filterText} />
<p>
<input type="checkbox" checked={this.props.inStockOnly} />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
前面我们已经确定好了state,接下来研究哪个组件可以包括和控制这几个state。
知识点来了:react中数据在组件结构中是自上而下的单向数据流。要搞清楚哪个组件控制哪个state不是一个简单快速的过程。对于初学者来说,这也可能是最具挑战性的、最费脑去理解的。
所以,以下列举了几个步骤去走一遍这个过程:
- 找出根据某个state去渲染数据的全部组件
- 找出公共状态组件(组件结构中联系到某个state的所有组件(第一步找出的组件)的上层组件)
- 第二步中公共状态组件或最上层组件都可以放置这个state
- 如果还是不能确定哪个组件放置state,可以创建一个新组件,放置在公共状态组件之上,用这个新组件放置state
让我们应用上边的步骤到示例app中:
- ProductTable需要根据state过滤、展示数据,searchBar需要展示搜索关键词和单选框状态
- 上一步两个组件的公共状态组件是FilterableProductTable
- 所以FilterableProductTable组件可以明证言顺的放置公共state
现在已经确定了state应该放在FilterableProductTable组件中。接下来先把{this.state = {filterText: '', inStockOnly: false}添加到这个组件的constructor中,设置app的初始化状态,然后将filterText和inStockOnly以prop属性的方式传入ProductTable和searchBar组件中,最后使用这些属性数据过滤ProductTable中的产品,显示searchBar中表单的值。
这一步做完就可以大致了解react app是如何运行的。将filterText设置为ball,然后刷新页面,就可以看到产品列表正确的显示结果。
STEP5:处理反向数据流
代码:
class ProductCategoryRow extends React.Component {
render() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
}
class ProductRow extends React.Component {
render() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
var rows = [];
var lastCategory = null;
console.log(this.props.inStockOnly)
this.props.products.forEach((product) => {
if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
return;
}
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextInputChange = this.handleFilterTextInputChange.bind(this);
this.handleInStockInputChange = this.handleInStockInputChange.bind(this);
}
handleFilterTextInputChange(e) {
this.props.onFilterTextInput(e.target.value);
}
handleInStockInputChange(e) {
this.props.onInStockInput(e.target.checked);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextInputChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockInputChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
this.handleFilterTextInput = this.handleFilterTextInput.bind(this);
this.handleInStockInput = this.handleInStockInput.bind(this);
}
handleFilterTextInput(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockInput(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextInput={this.handleFilterTextInput}
onInStockInput={this.handleInStockInput}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
到这一步,我们已经构建好了具备单向数据流的app,现在要做的是使app支持另一种数据流动方式:组件结构下层的表单组件去更新上层组件FilterProductTable中的state。
React中数据流动的方式规定很明确,从而让使用者更容易理解应用如何工作,但是相比双向数据绑定的框架就需要更多的代码。
如果现在在示例app中输入或单击选框,你会发现react忽视了你的操作。因为这里设置input标签prop属性的值是使用FilterProductTable组件的state传入的数据,这种方式中忽略输入的react规定的。
接下来想一想我们期望的运行方式和结果是什么,不管用户什么时候输入,state都会根据输入去更新。因为组件只能更新组件内自己的state,所以FilterProductTable组件需要传入回调函数
到searchbar组件中,当state需要更新时,调用回调。我们会在search Bar组件中使用onChange事件监听变化,然后传入的回调函数再调用setState(),最后app就会更新。
虽然上边的内容听起来很复杂,但是却只需要一小段代码。在react构建的app中,你的数据是如何流动和作用的,这一点是很清楚的。