用 React 思考:将设计原型拆解为组件与数据流
React 最令人兴奋的地方在于,它会彻底重塑你对网页和应用的构思方式。当你拿到设计师给出的原型图和后端提供的 JSON 数据时,不再是手忙脚乱地到处写 DOM 选择器和点击监听,而是有一套清晰严密的五步思考法则。
本关我们跟随官方经典案例——构建一个可搜索并能过滤库存的商品数据表(FilterableProductTable),完整走完这五步!
准备:设计原型与 JSON 数据
假设后端给你的 JSON 数据长成这样:
[
{ "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" }
]
设计师给出的原型图包含:
- 顶部一个搜索框,以及一个“仅展示有货商品”的复选框;
- 下方一个表格,包含两列:名称(Name)和价格(Price);
- 表格按品类(Fruits、Vegetables)展示分类表头;缺货的商品名称以红色高亮显示。
步骤一:将 UI 拆解为组件层级结构
第一步,拿起笔在原型图的每个模块周围画方框,并给它们命名:
FilterableProductTable(最外层大容器):包含整个应用的完整视图。SearchBar(搜索控制区):接收用户的搜索关键字输入和复选框点击。ProductTable(商品表格区):根据输入过滤并展示商品列表。ProductCategoryRow(分类表头行):展示品类标题(如 “Fruits”)。ProductRow(单个商品行):展示具体商品的名称和价格。
组件层级关系树:
FilterableProductTableSearchBarProductTableProductCategoryRowProductRow
设计原则(关注点分离):每个组件理想情况下只做一件事。当一个组件功能变得庞大时,就应该继续将其拆解为更小的子组件。
步骤二:使用 React 构建一个静态版本
有了层级结构后,先不要急着写任何交互或状态!最简单高效的办法是先根据数据模型构建一个纯展示的静态版本。
构建静态版本只需要写大量的 JSX 结构并用 Props 传递数据。绝对不要在这一步使用任何 state! 因为静态版本没有任何随时间变化的数据。
function ProductCategoryRow({ category }) {
return (
<tr>
<th colSpan="2">{category}</th>
</tr>
);
}
function ProductRow({ 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 }) {
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>
);
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" />
{' '}
Only show products in stock
</label>
</form>
);
}
export default function FilterableProductTable({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
自顶向下运行这段代码,一个精致完整、样式与数据就位的商品表格就已经展现在屏幕上了!
步骤三:找出 UI State 的最小且完整表示
现在要添加交互了。交互的核心是“数据随用户操作而改变”。为了让应用保持轻巧优雅,切忌定义过多重复或多余的 state。
我们把示例中出现的所有数据列出来:
- 原始商品列表(products);
- 搜索框中用户输入的文本(filterText);
- 复选框的勾选状态(inStockOnly);
- 过滤后的商品列表(filteredProducts)。
如何判断哪一个才是真正的 State?官方给出了 三条黄金法则:
- 它是通过 props 从父组件传递过来的吗? 如果是,那它不是 state。
- 它会随着时间推移保持不变吗? 如果一直不变,那它不是 state。
- 你能根据组件中已有的其他 state 或 props 计算出它吗? 如果能,那它绝对不应该是 state!
我们来逐一拷问:
- 原始商品列表:从父级作为 props 传入,且不会随搜索变化,不是 state。
- 搜索框文本:随用户键盘输入变化,且无法从别处计算出来,是 State!
- 复选框勾选状态:随用户点击切换布尔值,无法计算,是 State!
- 过滤后的商品列表:不是 State!因为只要拿着“原始商品列表”、“搜索文本”和“复选框状态”进行一次简单的
filter()计算就能实时得到它!如果把它存为 state,不仅冗余,而且极易出现搜索词变了但过滤结果忘记同步的 bug。
结论:整个应用只需要两个 State 变量:filterText 与 inStockOnly!
步骤四:确定 State 放置的位置
有了 State 之后,下一个关键问题是:这个 State 应该归哪个组件管?
React 使用单向数据流。寻找 State 归属的步骤如下:
- 找到所有需要根据该 state 进行渲染的组件:
ProductTable需要根据filterText和inStockOnly过滤列表;SearchBar需要根据它们显示当前输入框文字和复选框对勾。
- 找到这些组件的共同公共父组件(Common Parent Component):
FilterableProductTable是它们共同的直接父级!
- 因此,把 state 放在
FilterableProductTable中!
在父组件中声明:
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
然后把它们作为 props 传递给子组件:
<SearchBar filterText={filterText} inStockOnly={inStockOnly} />
<ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} />
步骤五:添加反向数据流(Inverse Data Flow)
现在数据能从顶层流向底层,但用户在底层的 SearchBar 中打字时,需要通知顶层的父组件更新 state。这就是反向数据流。
方法非常直接:父组件把改变自己 state 的函数(Setter 回调)作为 prop 传给子组件!
1. 父组件传递回调
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
2. 子组件监听原生输入事件并调用回调
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange
}) {
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>
);
}
每当用户在输入框打字,触发原生的 onChange,调用父组件传来的 setFilterText,更新父组件的 filterText,父组件重新渲染并把最新数据流向 ProductTable,完成整个闭环!
总结
这就是完整的“用 React 思考”五步法:
- 拆解层级:画框确定组件树;
- 静态呈现:用纯 Props 搭建结构;
- 精简状态:用三法则筛选最小完整 State;
- 定位归属:放在公共父组件中保持单向数据流;
- 反向闭环:通过回调函数让子组件驱动状态更新。