这是我自己系统整理的React系列博客,主要参考2023年3月开放的最新版本react官网内容,欢迎你阅读本系列内容,希望能有所收货。
本文是该系列的第3篇文章,阅读完本文后你将收获:
- 如何使用React逐步构建你的应用
- 了解props和state的概念
1 需求澄清
假如你现在是一个React项目的开发负责人,设计师已经根据需求设计出了一个UI界面,你需要按照需求开发如下界面:
这是一个菜单应用,能够展示商品列表及其价格,如果某商品没有库存了将以红色显示;另外,用户还可以根据上方的搜索框对结果进行过滤,或者勾选单选框以显示还有库存的商品。
本文将以这样一个需求为例,梳理React应用的开发步骤,帮助你快速上手。
2 根据UI界面划分组件
React应用是使用一个一个的组件拼装而成的,因此第一步要做的就是根据UI设计稿将页面拆分成一个个的Component。如下图所示,最外层的Component就是最终被调用的组件(FilterableProductTable),然后以该组件为根节点拆分成若干个子组件。
3 根据设计稿编写一个静态版本的代码
接下来就根据前一个阶段的分析结果,创建多个组件并将结果展示出来。注意,在这个阶段你可以先不考虑组件的交互性,只需要按照设计稿编写静态代码,能够演示的效果即可。因此你需要编写:
- FilterableProductTable.tsx
- SearchBar.tsx
- ProductTable.tsx
- ProductCategoryRow.tsx
- ProductRow.tsx
以上五个组件的静态代码,组件中呈现的数据可以不考虑接入真实的接口,可以直接使用Mock或者静态数据。由于组件之间存在嵌套关系,因此不可避免地,你需要使用props将数据从父组件传递给子组件,如:在ProductTable组件中肯定已经存储了商品列表products,然后该组件又将每个商品的数据传递给其子组件ProductRow或者ProductCategoryRow,这种时候就需要使用props了。
编写好的静态呈现代码可能如下所示:
/**
* 这是React.dev官网Quick Start中的Demo应用
* 对应章节:https://react.dev/learn/thinking-in-react
*
* @author Howard Wonnaut
* @date 2023-4-9
*/
import "./FilterableProductTable.css";
export type Product = {
category: string;
price: string;
stocked: boolean;
name: string;
}
function ProductCategoryRow({ category }: {category: string}) {
return (
<tr>
<th colSpan={2}>
{category}
</th>
</tr>
);
}
function ProductRow({ product }: {product: Product}) {
const name = product.stocked ? product.name :
<span style={{ color: 'red' }}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
function ProductTable({ products }: {products: Array<Product>}) {
const rows: any = [];
let lastCategory: string | null = 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>
);
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" />
{' '}
Only show products in stock
</label>
</form>
);
}
function FilterableProductTable({ products }: {products: Array<any>}) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
export default FilterableProductTable;
4 分析组件间的逻辑关系,给组件设置state状态
在前面的小节里,我们已经使用props将products自顶向下进行传递,并且将页面渲染了出来。接下来,我们需要考虑如何让这个页面可以交互起来,即根据输入框的值对结果进行过滤、根据单选框的状态对没有库存的结果进行显示/隐藏切换,为了达成这个目的,我们就需要使用state来存储对应的状态了。
我们可以根据如下三个问题来确定是否需要使用state来存储数据:
- 随着时间的推移,数据是否保持不变?如果是,那么不需要state
- 数据是否是父组件通过props传递过来的?如果是,那么不需要state
- 是否能够根据已经存在的state或者props计算出该数据?如果是,那么不需要state
根据上面的原则,我们梳理一下这个示例应用程序中的数据有哪些,以及是否需要使用state:
- 最外层的产品列表
products
:直接通过props传入即可,不需要state - 用户在搜索框输入的字符:会随着用户的输入而改变,且不能被计算出来,因此需要state
- 单选框的状态:会随着用户的操作而改变,且不能被计算出来,因此需要state
- 产品列表的筛选结果:可以根据
products
,输入框的字符和单选框状态计算得到,因此不需要state
基于上述分析,我们清楚了:输入框和单选框需要使用state保存其状态,其余的数据则不需要。
Props和State的对比
到这里,我们已经对该示例程序中的props和state进行了梳理,现将这二者的区别总结如下:
- Props类似你像函数传入的参数,将父组件的数据传递给子组件;
- State类似于一个组件的记忆,其允许组件保存某些信息,并且在用户进行交互操作之后更新存储的数据。例如:一个
Button
组件能够通过state存储其hover状态isHovered
。
接下来,我们需要再分析一下,应该将state放在哪个组件里面。由于搜索框和单选框都在SearchBar组件中,那么能不能直接把state放在该组件中呢?答案是不能,因为对于搜索框和单选框进行的交互操作都会影响ProductTable组件的呈现结果,如果将state保存在SearchBar中,ProductTable组件将无法及时感知到用户的操作行为,从而导致数据更新异常。因此,需要将state存放在SearchBar和ProductTable组件的公共父组件FilterableProductTable中,然后使用props将filterText和inStockOnly传递给子组件,此时,对应的代码为:
/**
* 这是React.dev官网Quick Start中的Demo应用
* 对应章节:https://react.dev/learn/thinking-in-react
*
* @author Howard Wonnaut
* @date 2023-4-9
*/
import { useState } from 'react'
import './FilterableProductTable.css'
export type Product = {
category: string
price: string
stocked: boolean
name: string
}
function ProductCategoryRow({ category }: { category: string }) {
return (
<tr>
<th colSpan={2}>{category}</th>
</tr>
)
}
function ProductRow({ product }: { product: Product }) {
const name = product.stocked ? (
product.name
) : (
<span style={{ color: 'red' }}>{product.name}</span>
)
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
)
}
function ProductTable({
products,
filterText,
inStockOnly,
}: {
products: Array<Product>
filterText: string
inStockOnly: boolean
}) {
const rows: any = []
let lastCategory: string | null = null
products.forEach((product) => {
// 如果只显示有库存的数据,并且当前商品无库存,直接不显示
if (inStockOnly && !product.stocked) {
return
}
// 如果过滤文本不在当前商品名称中存在,不显示该商品
if (
filterText &&
product.name
.toLocaleLowerCase()
.indexOf(filterText.toLocaleLowerCase()) === -1
) {
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>
)
}
function SearchBar({
filterText,
inStockOnly,
}: {
filterText: string
inStockOnly: boolean
}) {
return (
<form>
<input type="text" value={filterText} placeholder="Search..." />
<label>
<input type="checkbox" checked={inStockOnly} /> Only show products in
stock
</label>
</form>
)
}
function FilterableProductTable({ products }: { products: Array<any> }) {
const [filterText, setFilterText] = useState('')
const [inStockOnly, setInStockOnly] = useState(false)
return (
<div>
<SearchBar filterText={filterText} inStockOnly={inStockOnly} />
<ProductTable
filterText={filterText}
inStockOnly={inStockOnly}
products={products}
/>
</div>
)
}
export default FilterableProductTable
此时,只完成了数据自顶向下的传递,但是用户在SearchBar中的操作还没有传递到FilterableProductTable组件中,因此需要完善数据向上传递的链路,修改了FilterableProductTable和SearchBar组件的代码逻辑:
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}: {
filterText: string
inStockOnly: boolean
onFilterTextChange: Function
onInStockOnlyChange: Function
}) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}
/>{' '}
Only show products in stock
</label>
</form>
)
}
function FilterableProductTable({ products }: { products: Array<any> }) {
const [filterText, setFilterText] = useState('')
const [inStockOnly, setInStockOnly] = useState(false)
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
<ProductTable
filterText={filterText}
inStockOnly={inStockOnly}
products={products}
/>
</div>
)
}
最终的呈现效果如下,能够只显示有库存的商品:
能够根据输入文本对结果进行过滤:
本文完,希望能够对你有所帮助~