「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。
这是React清风拂面系列的第二篇,第一篇在这里
开始
本文主要通过一个例子来说明在使用React的时候,如何能用React编程思想指导我们更快更高效的实现功能。
先看下面的例子
一个常见的搜索列表,带有过滤功能。 其数据结构类似
const PRODUCTS =
[
{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },
{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]
- category: 种类
- price: 价格
- stocked:是否有库存
- name: 名称
如果我们要用React编程思想实现上面的例子,需要经历5个步骤。
步骤一:将UI分割成组件层级
我们拿到UI的第一步就是要将其分割成多个组件。
以上面的例子为例,我们可以将它分割成以下几个组件。
- FilterableProductTable(灰色)容器组件,包含整个app。
- SearchBar(蓝色)搜索条组件,接受用户输入,并有一个checkbox做筛选库存情况。
- ProductTable(紫色)下面列表的容器,用于展示列表。
- ProductCategoryRow(绿色) 显示每个类目。
- ProductRow(黄色) 具体的每个产品卡片。
组件层级的拆分应该体现UI的结构,因此上面几个组件的层级关系如下:
-
FilterableProductTable
-
SearchBar
-
ProductTable
ProductCategoryRow
ProductRow
-
步骤二:用React创建一个静态版本
构建组件层级可以用自顶向下和自底向上两种方式。
前者适合比较简单的场景,后者适合大型项目。
对于我们这个项目,适合自顶向下。组件层级按照我们上面拆分结构。
- 先设计 FilterableProductTable 组件
export default function App() {
return <FilterableProductTable products={PRODUCTS} />;
}
- 添加 搜索容器组件和列表容器组件
function FilterableProductTable({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
- 展示列表
function ProductTable({ products }) {
const rows = [];
let lastCategory = null;
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>
);
}
完整代码看这里
React 使用单向数据流来传递数据,在例子中所有的数据是由最顶层的FilterableProductTable一直往下传到ProductRow组件里面。
步骤三 找到最小集state
最小集state指的是尽可能让变量减少到最低。
在第一章指出几个原则:
- 只读的属性,不能定义在state里面。
- props传过来的属性,不能定义在state里面
- 可以根据其他state或者props计算的属性,不能定义在state里。
所以看下我们定义的几个组件。
- FilterableProductTable。容器型组件,传props给下面,不需要state。
- SearchBar有个input来记录用户输入,需要一个state,还有一个checkbox,来过滤库存,也需要一个state。
- ProductTable 需要展示初始化列表,另外如果checkbox被选中,需要展示过滤列表。初始化列表数据由props传过来,过滤列表可以由初始化列表和过滤条件计算得来,所以不需要state。
- 剩下2个组件,都是纯展示,不需要state。
所以,我们只需要输入框和checkbox两个state变量。
步骤四 在合适的地方定义state
解决了需要哪些state变量之后,接下来就是要解决在哪里定义这些变量。因为这两个变量直接是SearchBar的内容,我们的第一个反应就是直接在SearchBar定义即可,但是下面的ProductTable 也用到了这两个变量,所以如果在 SearchBar定义,如何与ProductTable 组件通信也是个问题。因此,需要考虑有没有其他可能。
React编程思想里面对于哪里定义state有自己的原则:
- 找出所有需要用到state的组件
- 找到它们的最近公共父组件
- 判断哪里定义state。
- 直接定义在最近公共父组件上
- 一直往上找,直到根组件
- 如果都不合适,在父组件的上面新建一个组件用来存在state
基于上面的原则,可以看到,如果我们有两个以上的组件用到了某个state,应该把state的定义位置上升到最近公共父组件,或者一直递归到根组件,或者新建一个祖先组件。
在我们这个例子里面,因为SearchBar和 ProductTable都用到了两个变量,他们的最近公共父组件是FilterableProductTable。所以定义在FilterableProductTable组件里面合适,完整代码见这里。
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
)
}
步骤五 反转数据流
React是单向数据流,上面我们定义的例子,保证了 FilterableProductTable 将 filterText 和 inStockOnly 传给了 两个子组件。但是 SearchBar 里面用户输入的值和是否存库 checkbox 的状态如何反馈回去。
针对这种情况,React 里面一般用回调函数来解决,父组件传给子组件一个props 函数,如果子组件里面需要改变父组件的state,回调一下这个props。
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
+ onFilterTextChange={setFilterText}
/>
然后input监听值的变动,调用 props 函数
<input
type="text"
value={filterText}
placeholder="Search..."
+ onChange={(e) => onFilterTextChange(e.target.value)} />
完整代码这里
总结
上面五个步骤揭露了如何用React思想编程,在我们日常开发中可以参考借鉴。