React设计哲学
使用 React 构建用户界面时,首先需要把它分解成一个个 组件,然后,你需要把这些组件连接在一起,使数据流经它们,在这个过程中,需要用到 hooks, props等React核心概念。
现在假设我们已经有了产品的原型,已经对应的接口数据返回
1 2 3 4 5 6 7 8
| [ { 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" } ]
|

1-将UI原型拆解为组件层级
这一步是将UI原型分割为多个组件,一般我们会根据功能来进行分割,一个组件理想情况下应仅做一件事情。
上面的原型可以分为:
- 顶级组件:整个过滤产品的表格
- 搜索框组件,获取用户输入进行过滤
- 产品表格组件,展示每个类别的产品和过滤的清单
- 表格表头组件:指明哪一类category
- 表格数据行:展示产品的名称name和价格price,并根据是否是库存改变颜色

2-构建静态页面
根据上述梳理的层级逻辑,我们可以直接编写静态逻辑
核心在于编写组件,通过props传递数据
静态版本并不需要引用state等hooks,因为这是交互层面的
此外,构建静态版本的页面,一般大型工程都是自底向上编写组件的
产品分类行
接收一个分类名称props
1 2 3 4 5 6 7
| function ProductCategoryRow({ category }) { return ( <tr> <th colSpan="2">{category}</th> </tr> ); }
|
产品数据行
接收产品对象作为props
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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> ); }
|
产品表组件
包含产品分类行和产品数据行,遍历产品数组,渲染分类行和实际数据行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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> ); }
|
搜索框
没有props,考察HTML基本功底
1 2 3 4 5 6 7 8 9 10
| function SearchBar() { return ( <form> <input type="text" placeholder="Search..." /> <label> <input type="checkbox" /> Only show products int stocked </label> </form> ); }
|
可过滤搜索的产品表
父级组件=搜索框+产品表组件
1 2 3 4 5 6 7 8
| function FilterableProductTable({ products }) { return ( <> <SearchBar /> <ProductTable products={products} /> </> ); }
|
产品列表静态数据
1 2 3 4 5 6 7 8
| 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" }, ];
|
渲染入口
1 2 3
| export default function App() { return <FilterableProductTable products={PRODUCTS} />; }
|
总结
上述我们实现的静态页面主要都是在写js以及html,基本就只用到了react中的props来进行父子组件数据传递,而且还是单项数据流
3-规划动态页面所需最简state
为了让页面能够根据用户交互动态进行渲染,我们需要用到react中的state hook,同时为了定义最简化的state,我们一般遵循如下的法制:考虑本次整个组件中每一条数据,在这个动态渲染的表格中,主要有这些数据:
- 产品原始列表
- 搜索框用户的输入
- 复选框的值
- 过滤后的产品数据列表
state判断方法论
其中哪些是 state 呢?标记出那些不是的:
- 随着时间推移 保持不变?如此,便不是 state。
- 通过 props 从父组件传递?如此,便不是 state。
- 是否可以基于已存在于组件中的 state 或者 props 进行计算?如此,它肯定不是state!
剩下的可能是 state。
所以:
- 产品原始列表:通过props传递,不是state
- 搜索框用户的输入:可以会是
- 复选框的值:可以会是,选中或者没选中
- 过滤后的产品数据列表:不是 state,因为可以通过被原始列表中的产品,根据搜索框文本和复选框的值进行计算。
4-验证state的位置
现在我们需要确定state应该被那些组件持有,设计的方法论如下:
- 确定哪些组件会用到state,在这里是ProductTable需要基于输入以及复选框来过滤产品列表;以及SearchBar需要展示state
- 寻找他们的最近父组件,一般都是放在这些组件的最近共同父组件上。(如果找不到一个有意义拥有这个 state 的地方,单独创建一个新的组件去管理这个 state,并将它添加到它们父组件上层的某个地方)
所以我们选择在公共父级组件:FilterableProductTable中定义state
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function FilterableProductTable({ products }) { const [filterText, setFilterText] = useState(""); const [inStockOnly, setInStockOnly] = useState(false); return ( <> <SearchBar filterText={filterText} inStockOnly={inStockOnly} /> <ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} /> </> ); }
|
之后在输入框中新增state的props来展示数据
1 2 3 4 5 6 7 8 9 10 11
| function SearchBar({ filterText, inStockOnly }) { return ( <form> <input type="text" placeholder="Search..." value={filterText} /> <label> <input type="checkbox" checked={inStockOnly} /> Only show products int stocked </label> </form> ); }
|
在产品表中也是新增state的props
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| function ProductTable({ products, filterText, inStockOnly }) { const rows = []; let lastCategory = null; products.forEach((product) => { if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) { return; } if (inStockOnly && !product.stocked) { 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> ); }
|
5-添加交互用反向数据流
但是上面的代码实际上只处理了渲染的动态逻辑,用户的交互并未实现,也就是用户在搜索框这个组件输入数据的时候,怎么将state数据反向传递到父级组件FilterableProductTable,然后再向下正向传递到ProductTable中
其实我们之前说的状态提升,本质上也就是将状态的更新反馈到父级组件,也就是反向数据流。
具体的实现方式就是:在父级组件中将state的更新函数通过props的方式传递到子组件,供子组件的回调函数调用
调整父组件,将回调函数作为props传递给搜索框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function FilterableProductTable({ products }) { const [filterText, setFilterText] = useState(""); const [inStockOnly, setInStockOnly] = useState(false); return ( <> <SearchBar filterText={filterText} inStockOnly={inStockOnly} onFilterTextChange={setFilterText} onInStockOnlyChange={setInStockOnly} /> <ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} /> </> ); }
|
搜索框内容变动时调用props的回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function SearchBar({ filterText, inStockOnly, onFilterTextChange, onInStockOnlyChange, }) { return ( <form> <input type="text" placeholder="Search..." value={filterText} onChange={(e) => onFilterTextChange(e.target.value)} /> <label> <input type="checkbox" checked={inStockOnly} onChange={(e) => onInStockOnlyChange(e.target.checked)} />{" "} Only show products int stocked </label> </form> ); }
|
至此我们已经实现了一个可以用于数据过滤的表格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| import { useState } from "react";
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, filterText, inStockOnly }) { const rows = []; let lastCategory = null; products.forEach((product) => { if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) { return; } if (inStockOnly && !product.stocked) { 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, onFilterTextChange, onInStockOnlyChange, }) { return ( <form> <input type="text" placeholder="Search..." value={filterText} onChange={(e) => onFilterTextChange(e.target.value)} /> <label> <input type="checkbox" checked={inStockOnly} onChange={(e) => onInStockOnlyChange(e.target.checked)} />{" "} Only show products int stocked </label> </form> ); }
function FilterableProductTable({ products }) { const [filterText, setFilterText] = useState(""); const [inStockOnly, setInStockOnly] = useState(false); return ( <> <SearchBar filterText={filterText} inStockOnly={inStockOnly} onFilterTextChange={setFilterText} onInStockOnlyChange={setInStockOnly} /> <ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} /> </> ); }
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" }, ];
export default function App() { return <FilterableProductTable products={PRODUCTS} />; }
|
总结
简单来说,props就是作为参数,state可以看作是组件的内存变量,通过props和state等hooks的组合实现多个组件组件间数据的共享
此外,本文还为如何从头开始设计一个可用的React组件提供了建设性的意见以及实用的方法论