React核心概念:以React的方式思考
引言
在我们看来React是构建大型快速反应的Web应用的首选方式。我们已经在Facebook和Instagram里面证实了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"}
];
步骤1:根据UI划分组件层次
第一件需要做的事是在设计稿上画出组件(包括子组件)并给他们命名。如果你有一个设计师,那么他们可能早就已经做好了这步工作,只要去问他们就可以了。可能PS上的图层名就是最终的组件名哦。
但是我们怎么知道什么才是组件呢?就像你决定是否创建一个函数或者对象一样,根据单一功能原则
来决定。组件应该只做一件事,如果组件包含了许多功能那么就需要将它再次分解成更小的组件。
由于我们经常向用户展示JSON数据模型,如果你的模型构建的准确,你都UI(或者说组件结构)就会与数据模型一一对应。这是因为UI与数据模型倾向于遵循相同的信息结构。将UI分成组件,其中组件需要与数据模型中的某一部分向匹配。
根据上图我们可以看到在用用中我们总共有5个组件。这里我们用斜体字表示每个组件所代表的含义:
- FilterableProductTable(橙色):作为整个应用的容器组件;
- SearchBar(蓝色):接收用户输入;
- ProductTable(绿色):根据用户输入展示对应的数据集合;
- ProductCategoryRow(蓝绿色):展示分类的标题;
- ProductRow(红色):展示一行产品;
现在我们看向ProductTable
,我们会发现表头(包含“Name”和“Price”)不是一个组件,但这可以根据个人喜好来决定是否要把它提成一个组件。在本例中,我们把它作为ProductTable
的一部分,因为渲染数据集合本就是ProductTable
的工作。但是如果表头是复杂的(比如包含有排序功能),那么将他提取成ProductTableHeader
就变得有必要了。
现在我们就定义好了我们的组件,让我们把它们按照层级排好。在设计稿中出现在其他组件内的组件在层级上应该作为该组件的子组件显式:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
步骤2:用React创建一个静态版本
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
const rows = [];
let lastCategory = null;
this.props.products.forEach((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>
);
}
}
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(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
现在我们已经有了组件的层级图了,接下来就该实现我们的应用了。最简单的方法是创建一版没有交互仅仅渲染了数据的UI。因为创建静态版本需要编写许多代码而且过程缺少思考,所以在这个过程中我们最好将他们解耦。而添加交互的过程不需要过多的编码,需要的是思考。接下来我们就来看看为什么需要这样做。
为了创建一个能够渲染数据模型的静态版本的应用,我们需要创建能够复用其他组件并且将数据作为props传递下去的组件。props是将数据从父组件传递给子组件的一种数据传递方式。如果你熟悉state的概念,请不要在构建静态版本时使用state,因为state存储的数据是变化的而这正适合交互时数据变化的特性,所以state需要为交互设计预留。所以在构建静态版本时我们不需要使用state。
你可以自上而下或者自下而上地构建应用,换而言之,就是你可以从构建组件层级中最高层开始(如FilterableProductTable
)或者从层级中较低层开始(如ProductRow
)。在简单的应用中,通常来说自上而下构建应用更加简单,但在大型应用中,自下而上是更好的方法,你可以直接测试你编写的部分。
在这步的最后,我们将会拥有用来构建应用的组件库。因为现在只是构建静态版本所以每个组件都只有render()
方法。最高层级组件FilterableProductTable
会将数据模型作为prop传递给其他组件。如果你修改了数据并且再次调用ReactDOM.render()
方法,那么UI将会被更新,你可以看到UI是如何根据数据来变化的。React的单向数据流(也成为单向绑定)使得组件模块化更加易于开发。
补充说明:props和state
理解React中的两种数据模型props和state是非常重要的。如果对于这两者的区别还不是很了解,可以查看state&生命周期,也可以查看FAQ:props和state的区别。
步骤3:确定UI状态的最小完备集
为了是你的UI具有交互能力,就需要具有对基本数据进行修改的能力。React通过state达到这个目的。
为了正确地构建你的应用,首先我们需要确定应用需要的最小可修改state集合。确定集合的关键是:不要重复数据
。确保你的应用可以通过这个数据集计算出其他所有需要的数据。比如,你现在证明构建一个待办事项清单,只需要保存待办事项的数组就可以了,不需要保存待办事项的数量,如果你想要知道待办事项的数目,只需要通过数组的length就可以知道了。
现在我们来看看应用中需要的数据,我们有:
- 产品列表
- 用户输入的搜索文本
- 单选框的值
- 筛选出来的产品列表
现在让我们看看哪一个数据可以作为state保存。在判断的时候问自己以下三个问题:
- 这是父组件通过props传递进来的吗?如果是,那它不是一个state;
- 它是否随时间推移保持不变?如果是,那它应该不是一个state;
- 你能根据其他state或者props推导出这个数据吗?如果是,那它肯定不是state;
产品列表是通过props传递的,所以它不是一个state。用户输入的搜索文本和单选框的值是可以随着时间改变的并且不发根据其他值计算出来,所以它们是state。最后,筛选后的产品列表不是state,因为它可以通过原始产品列表,用户输入的搜索文本和单选框的值计算出来。
所以,应用的state是:
- 用户输入的搜索文本
- 单选框的值
步骤4:确定state放置的位置
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
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(
<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() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText} />
<p>
<input
type="checkbox"
checked={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>
);
}
}
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(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
现在我们已经确定了应用的最小状态集,接下来我们需要确定哪个组件可以修改或者拥有这些state。
注意:React是单向数据流并且根据组件层级自上而下传递的。对于初次使用React的开发者来说可能无法快速清晰地了解什么组件应该拥有什么state,所以让我们按照下面的步骤来确定:
对于应用中的每个state:
- 找到根据这个state渲染页面的组件
- 找到他们共同的所有者组件(在组件层级上高于所有需要这个state的组件)
- 共同所有者组件或者更高层级的组件拥有这个sate
- 如果你无法找到能够合理拥有这个state的组件,那么就单独构建一个组件来持有这个state并将它放到层级高于共同所有者的地方
现在让我们将上述策略使用在我们的应用上:
ProductTable
需要根据state筛选产品列表,SearchBar
需要展示用户输入和单选框FilterableProductTable
是共同所有者- 把搜索文本和单选框值放在
FilterableProductTable
上是合理的
最终,我们把state放在FilterableProductTable
组件中。首先,在FilterableProductTable
的构造函数中添加属性this.state = {filterText: '', inStockOnly: false}
来展示应用的初始状态。之后将filterText
和inStockOnly
作为props传递给ProductTable
和SearchBar
。最后根据props在ProductTable
筛选出对应的产品,在SearchBar
中设置相应的值。
现在我们可以来看看应用是怎么表现的了,将filterText
的值设为ball
,然后刷新页面,你就可以看到产品列表更新了。
步骤5:添加反向数据流
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
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(
<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.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 (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
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 (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.handleFilterTextChange}
onInStockChange={this.handleInStockChange}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
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(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
到目前为止,我们已经根据自上而下传递的props和state正确地渲染了一个应用了。接下来让我们来使用另一种数据传递方式:在组件层级深处的表单组件需要在FilterableProductTable
处更新它的staet。
React显式地声明数据以使我们能够更好地了解程序是如何运行的,但这的确比传统的双向绑定繁琐一些。
如果你现在点击单选框或者在输入框内输入,你会发现页面不会有任何改变,React无视了你的操作。React是故意这么做的,因为我们设置了传递的prop值与FilterableProductTable
中的state值是相同的。
那么让我们来想想我们需要让程序如何运行。我们想要在用户输入时,能够更新state以反应用户的输入。因为组件只能更新自身的state,所以FilterableProductTable
需要向SearchBar
传递一个回调函数来实时更新state。我们可以使用<input>
标签上的onChange
方法来通知状态更新。在用户输入时,FilterableProductTable
传递的回调函数将会调用setState()
方法来更新应用状态。
这就是全部了
希望本章能够为你使用React构建组件和应用提供思路。尽管这相较以前来说需要写更多的代码,但请记住,阅读代码的时间往往比写代码的时间多得多。这样模块化,显式地编写代码能让你更加容易地阅读代码。当你开始编写大型的组件库时会清晰地意识到代码模块化,显式化的重要性。随着代码复用的增多,你的应用的行数也会逐渐减少。