这是 React 官方文档中的一章,为了加深理解所以翻译出来,原文在这儿。
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"}
];
第一步:把界面分解为部件层次
很可能你要做的第一件事,是在每个部件(子部件)周围画方框并为它们取名字。如果你和一名设计师一起工作,很可能他们已经这样做了。那么去和他们聊聊,或许他们Photoshop中图层的名字直接可以作为你的React部件的名字呢!
但你怎样定义一个部件呢?你日常编程中怎样决定创建一个函数或对象的?道理相同。一个类似的技术是功能单一原则(single responsibility principle), 意思是,一个部件应该只做一件事情。如果它越来越大,那么它应该被分为更小的部件。
由于你常常将JSON数据展示给用户看,你会发现,如果数据模型建得不错,你的UI(与你的部件结构)也相应的不会太差。原因是UI和数据模型往往依赖相同的信息架构,这也意味着把UI分解为部件常常不是太难,不过是根据数据模型来分解罢了。
你会看到我们这个简单的示例程序里有5个部件。
- FilterableProductTable(橙色):整个示例程序
- SearchBar(蓝色):接收所有的用户输入
- ProductTable(绿色):根据用户输入显示和过滤数据
- ProductCategoryRow(青绿色):显示类别
- ProductRow(红色):显示产品行
如果仔细看ProductTable
,会发现表头(Name和Price)不是它自己的部件。这是个见仁见智的问题,使用哪种方式还有争论。这个例子中,我们把它作为ProductTable的一部分,因为渲染数据集是ProductTable的责任。然而,如果这个表头过于复杂(如果以后我们增加点击表头排序),当然应该作为一个独立的部件ProductTableHeader来创建。
现在我们在原型中已经明确了部件,接下来把它们按照层次结构组织起来。原型中一个部件在另一个部件中,层次结构中应该为父子层级关系:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
第二步:建立静态版本
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
{category}
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
{product.name}
;
return (
{name}
{product.price}
);
}
}
class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
if (product.category !== lastCategory) {
rows.push(
);
}
rows.push(
);
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
class SearchBar extends React.Component {
render() {
return (
);
}
}
class FilterableProductTable extends React.Component {
render() {
return (
);
}
}
const 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(
,
document.getElementById('container')
);
现在你有了部件层级,是时候实现应用了。最容易的方法是先建立一个获取数据、渲染UI但没有交互的版本。把这些过程分离出来,是因为建立静态版本需要很多输入操作但不需要过多思考,增加交互功能不需要太多输入但需要很多思考。接下来我们会看到我这么说的原因。
建立渲染数据模型的静态版本,你需要创建使用其他部件的部件并且用props
来传递数据。props
是从父部件向子部件传递数据的一种方法。如果你对状态state
的概念熟悉,在创建应用的静态版本时一定别使用state
。状态只保留在交互的时候用。
你可以由底向上或从上到底开始。或者说,你可以首先创建最顶层的部件(例如从FilterableProductTable开始)或首先创建最底层部件(从ProductRow开始)。在简单的应用中,一般采取由上到底的方式;复杂的应用为了便于边创建边测试则相反。
这一步结束的时候,你会有了一个渲染数据模型的可重用部件库。因为这是应用的静态版,部件只包含render()
方法。最顶层的部件(FilterableProductTable
)或取数据模型为prop
。如果数据模型中的数据有改变,重新调用render()
,UI会相应的更新。静态版本复杂性不高,会很容易的看到UI如何更新。React单向数据流(one-way data flow
或one-way-binding
)保证了模块化和相应速度。
属性(Props)和状态(State)的插曲
React中有两种模型数据:props和state。理解两者之间的区别非常重要;进一步了解请参考官方文档。
第三步:确定最少(但功能齐全)的UI状态
使UI具备交互功能,需要底层数据触发事件。React的状态state
让这一点的实现很简单。
为了正确地创建应用,要首先思考应用需要的最小的状态变化。关键是别重复造轮子——DRY: Don’t Repeat Yourself. 找出应用需要的最少的数据,据此在计算其他的。例如,如果要创建TODO列表,只要有个保存TODO项目的数组即可,不需要TODO项目数量的数据。因为数量可以由获取数组长度很容易地得到。
考虑我们这个例子中需要的数据,我们有了:
- 产品原始列表
- 用户输入的搜索文本
- 复选框的值
- 过滤的产品列表
我们逐一分析,看看哪个是状态。对每一个数据,只要问三个问题:
- 它是父部件经由props传递给子部件的吗?如果是,很可能不是状态。
- 它的值在应用操作过程中会改变吗?如果不会,很可能不是状态。
- 它的值能由其他状态或属性计算得到吗?如果是,很可能不是状态。
原始数据列表经props传入,那它不是状态。搜索文本和复选框的值会在应用操作过程中被改变,而且不能由其他属性或状态计算获得,看起来是状态。最后,过滤的产品列表不是状态,因为它可以经过计算原始数据列表、搜索文本和复选框的值获得。
最后,我们的状态是:
- 用户输入的搜索文本
- 复选框的值
第四步:确定状态的位置
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
{category}
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
{product.name}
;
return (
{name}
{product.price}
);
}
}
class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
);
}
rows.push(
);
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
class SearchBar extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (
);
}
}
const 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(
,
document.getElementById('container')
);
我们确定了应用中的最少的状态,接下来,我们确定这些状态属于哪个部件。
记住:React的部件中数据是单向由顶向下流动。哪些部件传递这些状态可能不能马上弄清楚。这往往是新手理解起来最难的部分,按照下面的流程确定:
对于应用中每一个状态:
- 确定依赖这个状态来渲染的每一个部件
- 寻找共同的父部件(在部件层级中,位于所有需要这个状态的部件之上的父部件)
- 或者拥有这些状态的层级更高的部件
- 如果找不到拥有这个状态的部件,创建一个持有这个状态的新部件,加到部件层级中,位置在共同父部件之上。
我们根据上面的原则检视一下:
- ProductTable需要根据状态过滤产品,SearchBar需要显示搜索文本和复选框状态
- 它们共同的父部件是FilterableProductTable
- 过滤文本和复选框值放在FilterableProductTable看起来有意义
酷,那么我们决定把状态放在FilterableProductTable中。首先,在FilterableProductTable构造器constructor中增加this.state = {filterText: '', inStockOnly: false}
来设置应用的初始状态。接着,将filterText
和inStockOnly
作为属性传递到ProductTable和SearchBar中。最后,用这些属性过滤ProductTable的数据,同时显示在SearchBar表单中。
你会开始看到应用如何反应:设置filterText
为“ball”
然后刷新应用。你会看到数据表正确地刷新了。
第五步:添加反向数据流
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
{category}
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
{product.name}
;
return (
{name}
{product.price}
);
}
}
class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
);
}
rows.push(
);
lastCategory = product.category;
});
return (
Name
Price
{rows}
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(e) {
this.props.onFilterTextChange(e.target.value);
}
handleInStockChange(e) {
this.props.onInStockChange(e.target.checked);
}
render() {
return (
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockChange(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}
render() {
return (
);
}
}
const 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(
,
document.getElementById('container')
);
现在为止,我们创建的这个应用能够根据属性和状态正确地渲染。现在是时候支持反向数据流了:在部件层级内部的表单需要更新FilterableProductTable状态。
React使这个数据流清晰易懂,以便理解你的程序是如何工作的,但是它需要比传统的双向数据绑定更多的输入。
如果你尝试在当前版本的示例中键入或选中该框,则会看到React忽略了你的输入。这是因为我们已经将输入的值prop设置为始终等于从FilterableProductTable传入的状态。
让我们想想我们希望发生的事。我们希望确保每当用户更改表单时,我们都会更新状态以反映用户的输入。由于组件应该只更新自己的状态,FilterableProductTable会将回调传递给SearchBar,只要状态更新就会触发。我们可以使用输入上的onChange事件来通知它。FilterableProductTable传递的回调将调用setState(),应用将被更新。
虽然这听起来很复杂,实际上只是几行代码。这真的使数据如何在整个应用程序中如何流动一目了然。
结语
希望这可以让你了解如何用React来构建组件和应用。 尽管可能需要会比以前更多地输入内容,但请记住,代码的可读性远远比代码的编写重要,读取模块化的显式代码非常容易。当你开始构建大型组件库时,将会体会到这种明确性和模块性,通过代码重用,你的代码行将开始缩小。