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"}
];
一.划分组件层级
FilterableProductTable(橙色): 是整个示例应用的整体SearchBar(蓝色): 接受所有的用户输入ProductTable(绿色): 展示数据内容并根据用户输入筛选结果ProductCategoryRow(天蓝色): 为每一个产品类别展示标题ProductRow(红色): 每一行展示一个产品
-
FilterableProductTable-
SearchBar -
ProductTableProductCategoryRowProductRow
-
第二步:用 React 创建一个静态版本,自下由上创建静态版本结构
创建 分类组件(分类名称)
创建商品内容组件
创建商品展示区
要注意:创建新元素时,需要给它设置一个特殊的 key 属性,key 帮助 React 识别哪些元素改变了,比如被添加或删除,详情请看react.docschina.org/docs/lists-…
创建商品搜索区
创建父组件,引入子组件
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' }
];
确定 state 放置的位置
对于应用中的每一个 state:
- 找到根据这个 state 进行渲染的所有组件。
- 找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。
- 该共同所有者组件或者比它层级更高的组件应该拥有该 state。
- 如果你找不到一个合适的位置来存放该 state,就可以直接创建一个新的组件来存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。
根据以上策略重新考虑我们的示例应用:
ProductTable需要根据 state 筛选产品列表。SearchBar需要展示搜索词和复选框的状态。- 他们的共同所有者是
FilterableProductTable。 - 因此,搜索词和复选框的值应该很自然地存放在
FilterableProductTable组件中。
很好,我们已经决定把这些 state 存放在 FilterableProductTable 组件中。首先,将实例属性 this.state = {filterText: '', inStockOnly: false} 添加到 FilterableProductTable 的 constructor 中,设置应用的初始 state;接着,将 filterText 和 inStockOnly 作为 props 传入 ProductTable 和 SearchBar;最后,用这些 props 筛选 ProductTable 中的产品信息,并设置 SearchBar 的表单值。
你现在可以看到应用的变化了:将 filterText 设置为 "ball" 并刷新应用,你能发现表格中的数据已经更新了。
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() {
// 父级传值,使用this.props读取数据
const product = this.props.product;
// stocked:false 无库存。界面显示为红色
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() {
/*
filterText 搜索框文本
inStockOnly 存货状态
*/
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 属性
key={product.category} />
);
}
rows.push(
<ProductRow
product={product}
// 当你创建一个元素时,必须包括一个特殊的 key 属性
key={product.name} />
);
// 遍历完成后 设置lastCategory为最后一个遍历的分类的值,避免重复push分类组件
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) {
/*
绑定两个函数
handleFilterTextChange 监听搜索框文字内容变化
handleInStockChange 监听 存货按钮状态变化
*/
super(props);
// 在 JavaScript 中,class 的方法默认不会绑定 this。不绑定的话会出现 undefined
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() {
/*
由于这个组件的传值是由 该组件的父组件传值过来的 所以需要使用this.props获取
设置两个参数的作用:
1.接受 父组件 传值过来的数据
2.绑定组件内部元素的值,当组件内部元素状态或数据发生变化时,React会监听并同步给父组件,
实现 state 来实现交互效果
*/
// const filterText = this.props.filterText;
// const inStockOnly = this.props.inStockOnly;
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
// 监听搜索框内容变化,调用函数实时改变FilterText的值
onChange={this.handleFilterTextChange}/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
// 创建FilterableProductTable父组件 引入搜索组件和商品组件作为子组件
class FilterableProductTable extends React.Component {
constructor(props) {
// ES6语法 继承参数
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')
);