Thinking in React
本文翻译自React的官方博客,详情请阅读原文。
React非常适合构建组件化的应用,它注重高性能,因此组建的重用,项目的扩展都十分灵活,Facebook和instagram的不少商业项目使用了此框架。
本文主要通过“输入查询数据”这个简单的demo来说明或者学习如何用React来架构。
数据模型
我们需要根据JSON API来显示并且操作数据,最终的可视化操作是基于JSON数据的基础之上。最终的效果图如下:
以下便是我们模拟的JSON数据:
[ {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为组件继承
我们如何确定哪一部分应该为一个组件呢?我们可以遵循“单一职责”原则,也就是说,理想的组件只做一件事情。组件也应该根据数据(model)的结构灵活进行设计,这样最终的UI匹配提供的数据(model),利于维护和理解。
如上图所示,我们将这个应用分为5个组件。
FilterableProductTable
(orange): 包含所有的子组件,是个容器SearchBar
(blue): 用于用户输入交互ProductTable
(green): 呈现数据项并根据用户输入过滤数据ProductCategoryRow
(turquoise): 显示条目信息ProductRow
(red): 显示产品的具体信息
我们可以看到,tHead部分(Name和Price)并不是一个单独的组件,在这个例子中,之所以tHead属于ProductTable组件是因为它并没有与数据(model)有关联,考虑这种情况,如果要单击tHead部分的表头实现表格内容的排列,我们最好为tHead单独设计一个组件,并在该组件上绑定事件处理函数。
至此,我们将这五个组件的继承关系确定下来:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
step2 创建静态版本
有了组件的继承关系,我们首先创建一个静态版本的应用。我们可以通过组件复用以及父子组件之间的props通信来完成模型数据的渲染。props是父子组件通信的一种方式,如果你也了解state特性的话,那么一定不要使用state来构建静态版本,state用于创建交互版本,也就是说,state中的数据会随着时间而改变,下面的一节会讲解何时将数据放入state中。
我们可以自顶向下或者自下而上来构建应用,在做测试时我们可以自下而上来进行每个模块的测试,而一般构建应用我们则是采用自顶向下的模式,结合数据的自上而下传递,利于开发。
在这一步,由于我们构建的是静态版本,因此每个组件只实现了其render方法,用以基本的数据渲染。最顶层的组件(FilterableProductTable)的props中存入要渲染的数据模型,每当模型数据发生改变时,会对应的视图层的改变,这也正是React所提出的的单向数据流模型(one-way data flow)。
在React中,组件有两种类型数据--props和state。它们之间的具体区别可以参考官方文档。
var ProductCategoryRow = React.createClass({ render: function() { return (<tr><th colSpan="2">{this.props.category}</th></tr>); } }); var ProductRow = React.createClass({ render: function() { 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> ); } }); var ProductTable = React.createClass({ render: function() { 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> ); } }); var SearchBar = React.createClass({ render: function() { return ( <form> <input type="text" placeholder="Search..." /> <p> <input type="checkbox" /> {' '} Only show products in stock </p> </form> ); } }); var FilterableProductTable = React.createClass({ render: function() { 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'} ]; React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
step3 确定组件的state
为了使应用具有动态交互性,必须将状态的改变(用户的输入或者单击操作等)反映到我们的UI上,通过React给组件提供的state完成上述需求。
我们首先要确定应用的可变的状态集合,遵循DRY原则:don't repeat youself。我们需要考虑应用中的所有的数据,它包括:
- 基本的产品列表
- 用户输入的过滤条件
- checkbox的值
- 过滤后的产品列表
根据下面条件选择哪些数据可以作为state:
- 是否通过父组件通过props传递,如果是,则不是state
- 是否随着时间而改变,如果不变,则不是state
- 可以通过其他state或者props计算得到,如果可以,则不是state
产品数据列表是通过父组件的props传递,因此不是state,用户输入和checkbox满足上述三个条件,可以作为state,二对于过滤的列表,则可以根据产品数据和用户输入来获取到,因此不是state。
故,input输入值和checkbox的值可以作为state。
step4 确定state所属的组件
目前确定了state集合,接下来需要确定究竟是哪个组件拥有这个state,或者随着state而变化。
我们要明确React的单项数据流是沿着组件继承链流动的,这有时很难确定哪一个组件拥有这个state,不过我们可以根据以下原则来大体确定state所属的组件。
在每一个状态期,
- 确保每个组件都会根据当前状态来渲染
- 寻找其共同的祖先组件
- 在继承链中层级较高的组件拥有state
回到我们的应用中,
ProductTable需要根据state来过滤数据,
SearchBar
需要显示输入的文字和选项.- 这两个组件的共同祖先是
FilterableProductTable
. - 因此state的集合应该所属
FilterableProductTable组件
所以,我们确定了state所属的组件是FilterableProductTable。我们需要给该组件设置getInitialState方法设置组件的初始状态,并且通过props将状态传递给ProductTable和SearchBar,最后我们就可以在ProductTable和SearchBar中获取状态并根据当前状态显示相应的数据。
var ProductCategoryRow = React.createClass({ render: function() { return (<tr><th colSpan="2">{this.props.category}</th></tr>); } }); var ProductRow = React.createClass({ render: function() { 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> ); } }); var ProductTable = React.createClass({ render: function() { var rows = []; var lastCategory = null; this.props.products.forEach(function(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; }.bind(this)); return ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{rows}</tbody> </table> ); } }); var SearchBar = React.createClass({ render: function() { 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> ); } }); var FilterableProductTable = React.createClass({ getInitialState: function() { return { filterText: '', inStockOnly: false }; }, render: function() { 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'} ]; React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
step5 添加反向数据流
等等,目前构建的应用并不能通过表单来反向设置state,因此,我们无法再input标签输入任何值。这就需要我们手动进行反向数据设置。React默认的单项数据流是从model渲染到UI,而通过UI来设置model则需要手动编写,主要的操作就是通过获取组件对应的DOM对象,获取当前DOM的属性值并反向设置state来完成。
当前版本的应用,React会忽略输入值和选定值,这是理所当然的,因为我们在FilterableProductTable中设置的state初始值为filterText=‘’,inStockOnly=false,所以对于ProductTable和SearchBar而言,也就是针对这两个值渲染,但是由于通过input和checkbox的输入并未改变这两个state的值,因此,这两个组件其实并没有被渲染。
所以我们通过在ProductTable和SearchBar设置事件监听函数,并且每当函数触发时setState当前的状态,促使组件渲染重绘,完成数据的动态呈现。在具体实现中,可以通过refs锚点来获取具体的具名组件,并通过调用组件的getDOMNode方法,获取对于DOM对象并据此设置新的state。
/** @jsx React.DOM */ var ProductCategoryRow = React.createClass({ render: function() { return (<tr><th colSpan="2">{this.props.category}</th></tr>); } }); var ProductRow = React.createClass({ render: function() { 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> ); } }); var ProductTable = React.createClass({ render: function() { console.log(this.props); var rows = []; var lastCategory = null; this.props.products.forEach(function(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; }.bind(this)); return ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{rows}</tbody> </table> ); } }); var SearchBar = React.createClass({ handleChange: function() { this.props.onUserInput( this.refs.filterTextInput.getDOMNode().value, this.refs.inStockOnlyInput.getDOMNode().checked ); }, render: function() { return ( <form> <input type="text" placeholder="Search..." value={this.props.filterText} ref="filterTextInput" onChange={this.handleChange} /> <p> <input type="checkbox" checked={this.props.inStockOnly} ref="inStockOnlyInput" onChange={this.handleChange} /> {' '} Only show products in stock </p> </form> ); } }); var FilterableProductTable = React.createClass({ getInitialState: function() { return { filterText: '', inStockOnly: false }; }, handleUserInput: function(filterText, inStockOnly) { this.setState({ filterText: filterText, inStockOnly: inStockOnly }); }, render: function() { return ( <div> <SearchBar filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} onUserInput={this.handleUserInput} /> <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'} ]; React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
对,就是这样
例子虽然非常简单,但是里面蕴含的思想确实值得玩味。组件的设计,数据的传递,状态集的确定,双向数据的传递以及事件处理和获取具名组件等等技术都包含在内,如果真的吃透了这个例子,那么我想在今后的可重用敏捷开发之路上必定又有新的收获,具体到我们的实现上就是组件设计的更为优美,代码量更为精少。