React|Thinking in React

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 }) {
//JSX三元运算
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 = [];
//save lastCategory to avoid duplication rendering
let lastCategory = null;
//handle products data, split to categories and product item rendering
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 = [];
//save lastCategory to avoid duplication rendering
let lastCategory = null;
//handle products data, split to categories and product item rendering
products.forEach((product) => {
//Input value filtering
if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {
return;
}
//when checkbox checked show only stocked product
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 = [];
//save lastCategory to avoid duplication rendering
let lastCategory = null;
//handle products data, split to categories and product item rendering
products.forEach((product) => {
//Input value filtering
if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {
return;
}
//when checkbox checked show only stocked product
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组件提供了建设性的意见以及实用的方法论


React|Thinking in React
http://example.com/2025/02/05/React-Thinking-in-React/
作者
Noctis64
发布于
2025年2月5日
许可协议