函数组件设计模式
# 函数组件设计模式
# 容器模式
# 定义
容器模式,就是把原来需要条件运行的 Hooks 拆分成子组件,然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。
这在一些复杂的场景之下,也能达到拆分复杂度,让每个组件更加精简的目的。
# 举例
Hooks 有一个重要规则,即:Hooks 必须在顶层作用域调用,而不能放在条件判断、循环等语句中,同时也不能在可能的 return
语句之后执行。
但有时候我们却需要先进行条件判断,再决定是否调用相应的代码:比如一个对话框组件,通过 visible
属性来控制是否显示:
import { Modal } from "antd";
import useUser from "../useUser";
function UserInfoModal({ visible, userId, ...rest }) {
// 当 visible 为 false 时,不渲染任何内容
if (!visible) return null;
// 这一行 Hook 在可能的 return 之后,会报错!
const { data, loading, error } = useUser(userId);
return (
<Modal visible={visible} {...rest}>
{/* 对话框的内容 */}
</Modal>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上述代码会报错,因为在 return
语句之后使用了 useUser
这个 Hook。
要想实现我们期望的逻辑,就可以采用容器模式。具体的做法是把条件判断的结果放到两个组件之中,针对上面的例子,就可以在 UserInfoModal
外层加一个容器:
// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return <UserInfoModal visible {...rest} />;
}
2
3
4
5
6
7
8
9
10
这样就间接实现了按条件去执行 Hooks 的逻辑。
# 对比
如果只有一个属性 visible
来决定是否展示的话,也可以直接在父级 visible && <UserInfoModal />
。
但在实际的使用场景中,可能判断条件不止 visible
一个属性,而会是一些属性的组合,来具体决定 render 什么内容。这样的情况使用容器模式就有一些好处了,比如说在函数组件可以少写一些条件判断语句,并且确保每个组件尽量短小,这样反而更加易读和维护。
另外,容器模式的隔离对象是多个子组件,它通常用于一些比较大块逻辑的隔离。对于一些比较细节的控制,其实也可以把判断条件放到 Hooks 中去,比如:
function useUser(id) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return
// 只有 id 存在,才进入获取用户信息的逻辑
});
}
2
3
4
5
6
7
8
9
10
# render props 模式
# 定义
render props 模式和 Hooks 没有任何关系,但它可以作为 Hooks 的一个补充。
因为 Hooks 只能用作数据逻辑的重用,而 render props 可以用于 UI 表现逻辑的重用。
# 举例
比如下图的两个列表展示,一个展示名字,一个展示用户。它们都有一个共同点:如果超过一定数量,就把多余的部分折叠起来,通过一个弹出框去显示。
(render props 案例)
提炼一下需求:
- 相同点:数据超过一定数量时,显示一个「更多...」的文字;鼠标移上去则弹出一个框,用于显示其它的数据。
- 不同点:每一个列表项如何渲染,是在使用的时候决定的(直接罗列、列表、表格等各种形式)。
通过 render props 这个设计模式,来实现这个 ListWithMore
组件:
import { Popover } from "antd";
function ListWithMore({ renderItem, data = [], max }) {
const elements = data.map((item, index) => renderItem(item, index, data));
const show = elements.slice(0, max);
const hide = elements.slice(max);
return (
<span className="list-with-more">
{show}
{hide.length > 0 && (
<Popover content={<div style={{ maxWidth: 500 }}>{hide}</div>}>
<span className="more-items-wrapper">
and{" "}
<span className="more-items-trigger"> {hide.length} more...</span>
</span>
</Popover>
)}
</span>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个组件接收了三个参数:
- renderItem:用于接收一个函数,由父组件决定如何渲染一个列表项。
- data:需要渲染的数据。
- max:最多显示几条数据。
接下来让父组件调用,实现上面示意图中两个场景的需求:
// 这里用一个示例数据
import data from './data';
function ListWithMoreExample () => {
return (
<div className="list-with-more">
<h1>User Names</h1>
<div className="user-names">
Liked by:{" "}
<ListWithMore
renderItem={(user) => {
return <span className="user-name">{user.name}</span>;
}}
data={data}
max={3}
/>
</div>
<br />
<br />
<h1>User List</h1>
<div className="user-list">
<div className="user-list-row user-list-row-head">
<span className="user-name-cell">Name</span>
<span>City</span>
<span>Job Title</span>
</div>
<ListWithMore
renderItem={(user) => {
return (
<div className="user-list-row">
<span className="user-name-cell">{user.name}</span>
<span>{user.city}</span>
<span>{user.job}</span>
</div>
);
}}
data={data}
max={5}
/>
</div>
</div>
);
};
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
可以看到,代码里使用了两个 ListWithMore
组件,通过 renderItem
这个属性,我们可以自主决定该如何渲染每一个列表项,从而把一部分 UI 逻辑抽象出来,形成一个可复用的逻辑,以简化不同场景的使用。
(完)