本文摘自 react哲学。
后台数据
[
{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"}
];
UI
将UI划分成组件
划分原则:一个组件只做一件事情,如果一个组件做的事情太多,考虑将它拆分成多个组件。
好了,组件层级现在清楚了。
接下来,可能会有两个疑问:刚开始接触react时我脑子里常常有这俩疑问
-
应该 从上往下开发,还是从下往上开发?
如果应用比较简单,可以从上往下;如果应用比较复杂,最好从下往上。 -
写组件时,什么时候应该用
state
,什么时候应该用props
?
对不起,我答不上来。
但如果两步走写组件,这个问题可能好回答一些。
哪两步?先 实现静态页面,再 添加交互功能。
实现静态页面
实现静态页面时,不用考虑交互功能,也就是说,此时没有动态数据。
而state
是变化的数据,这就意味着,在实现静态页面时,如果要传递数据,只能用props
。
- ProductRow
显示产品的name
、price
属性,且stocked
为false
时,name
属性字体染红。
class ProductRow extends React.Component{
render(){
let {name,price,stocked} = this.props.product;
const style = {
color:'red'
}
name = stocked?name:<span style={style}>{name}</span>;
return (
<tr>
<td>{name}</td>
<td>{price}</td>
</tr>
)
}
}
- ProductCategoryRow
class ProductCategoryRow extends React.Component{
render(){
const category = this.props.category;
return (
<tr>
<th colSpan='2'>{category}</th>
</tr>
)
}
}
- ProductTable
class ProductTable extends React.Component{
render(){
const rows = [];
let lastCategory = null;
this.props.products.forEach(product => {
if(product.category!==lastCategory){
rows.push(
<ProductCategoryRow
key={product.category}
product={product}/>
)
}
rows.push(<ProductRow
key={product.name}
product={product} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
)
}
}
这里之所以可以根据 product.category
是否等于lastCategory
来进行分组,得益于后台数据已经分好类排好序了。
如果没有分好类排好序,可以这么写:
class ProductTable extends React.Component{
render(){
const map = new Map();
this.props.products.forEach(product => {
const {category,name} = product;
let rows = map.get(category);
if(!rows){
rows = [<ProductCategoryRow key={category}
category={category}/>];
map.set(category,rows)
}
rows.push(
<ProductRow key={name}
product={product} />
);
});
const allRows = [...map.values()].reduce((accum,item)=>[...accum,...item],[]);
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{allRows}
</tbody>
</table>
)
}
}
- SearchBar
class SearchBar extends React.Component{
render(){
const {filterText,isStockOnly} = this.props;
return (
<form>
<input type='text'
placeholder="Search..."/>
<p>
<input type='checkbox' /> Only show products in stock?
</p>
</form>
)
}
}
- FilterProductTable
class FilterProductTable 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: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterProductTable products={PRODUCTS}/>,
document.getElementById('root')
);
添加交互功能
React的理念是UI=render(data)
,数据驱动视图。
交互功能就是通过改变数据实现的,而这个变动的数据就是state
。
state的最小表示
state
最小表示,个人理解就是 没有冗余,必须有,一个都不能少的数据。
就拿本例来说,我们会关注四个数据:
- 全部产品数据
- 是否过滤
- 过滤字符串
- 过滤后的产品数据
全部产品数据是由后台给的,如果要过滤,只要知道过滤字符串,就能推导出过滤后的产品数据,所以 过滤后的产品数据不是 state
。
一个数据应不应该是state
,react哲学给我们提供了以下判断思路:
- 如果数据是通过
props
从父组件传递过来的,那么这个数据就不应该是state
- 如果数据是静态的,不会变动的,那么这个数据也不应该是
state
- 如果数据可以根据其他
props
或state
推导计算出来,那么这个数据也不应该是state
再回到本例,之前我们确定了 过滤后的产品数据 不是 state
。
全部产品数据是后台给的,且通过props
传递给了最高层组件FilterableProductTable
,所以它不是state
。
这样一来,最后入选state
就是 是否过滤isStockOnly
和 过滤字符串filterText
。
state应该放哪儿
提取组件与状态提升这篇有介绍过:通常组件维护着自己的state
,但如果其他组件也需要这个state
,就应该把这个state
提升到它们的最近共同父组件里。
react哲学建议:如果实在找不到合适的位置,就新建一个组件来放state
,且该组件的层级比共同父组件的层级高。这让我想到了redux的<Provider>
好吧,所以把isStockOnly
和filterText
放到组件FilterableProductTable
里。
完整实现
import React from "react";
import ReactDOM from "react-dom";
import './index.css';
class ProductRow extends React.Component{
render(){
let {name,price,stocked} = this.props.product;
const style = {
color:'red'
}
name = stocked?name:<span style={style}>{name}</span>;
return (
<tr>
<td>{name}</td>
<td>{price}</td>
</tr>
)
}
}
class ProductCategoryRow extends React.Component{
render(){
const category = this.props.category;
return (
<tr>
<th colSpan='2'>{category}</th>
</tr>
)
}
}
class ProductTable extends React.Component{
render(){
const {filterText,isStockOnly} = this.props;
const map = new Map();
this.props.products.forEach(product => {
const {category,name,stocked} = product;
if(name.toLowerCase().indexOf(filterText.toLowerCase())===-1){
return;
}
if(isStockOnly && !stocked){
return;
}
let rows = map.get(category);
if(!rows){
rows = [<ProductCategoryRow key={category}
category={category}/>];
map.set(category,rows)
}
rows.push(
<ProductRow key={name}
product={product} />
);
});
const allRows = [...map.values()].reduce((accum,item)=>[...accum,...item],[]);
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{allRows}
</tbody>
</table>
)
}
}
class SearchBar extends React.Component{
constructor(props){
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleStockChange = this.handleStockChange.bind(this);
}
handleFilterTextChange(e){
this.props.onFilterTextChange(e.target.value);
}
handleStockChange(e){
this.props.onStockChange(e.target.checked);
}
render(){
const {filterText,isStockOnly} = this.props;
return (
<form>
<input type='text'
placeholder="Search..."
value={filterText}
onChange={this.handleFilterTextChange}/>
<p>
<input type='checkbox'
checked={isStockOnly}
onChange={this.handleStockChange}
/> Only show products in stock?
</p>
</form>
)
}
}
class FilterProductTable extends React.Component{
constructor(props){
super(props);
this.state = {
filterText:'',
isStockOnly:false
}
this.onFilterTextChange = this.onFilterTextChange.bind(this);
this.onStockChange = this.onStockChange.bind(this);
}
onFilterTextChange(filterText){
this.setState({
filterText
});
}
onStockChange(isStockOnly){
this.setState({
isStockOnly
});
}
render(){
const {filterText,isStockOnly} = this.state;
return (
<div>
<SearchBar
filterText={filterText}
isStockOnly={isStockOnly}
onFilterTextChange={this.onFilterTextChange}
onStockChange={this.onStockChange}
/>
<ProductTable
filterText={filterText}
isStockOnly={isStockOnly}
products={this.props.products}/>
</div>
)
}
}
const PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterProductTable products={PRODUCTS}/>,
document.getElementById('root')
);