Skip to main content

函数组件设计模式:如何应对复杂条件渲染场景?

容器模式:实现按条件执行 Hooks

import { Modal } from "antd";
import useUser from "../09/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>
);
}

无法通过编译,因为在 return 语句之后使用了 useUser 这个 Hook。

因为 Hooks 使用规则的存在,使得有时某些逻辑无法直观地实现。换句话说,Hooks 在带来众多好处的同时,也或多或少带来了一些局限。因此,我们需要用一个间接的模式来实现这样的逻辑,可以称之为容器模式

具体做法就是把条件判断的结果放到两个组件之中,确保真正 render UI 的组件收到的所有属性都是有值的。

针对刚才我们讲的例子,就可以在 UserInfoModal 外层加一个容器,这样就能实现条件渲染了。实现的代码如下:

// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return <UserInfoModal visible {...rest} />;
}

在容器模式中我们其实也可以看到,条件的隔离对象是多个子组件,这就意味着它通常用于一些比较大块逻辑的隔离。所以对于一些比较细节的控制,其实还有一种做法,就是把判断条件放到 Hooks 中去

总体来说,通过这样一个容器模式,我们把原来需要条件运行的 Hooks 拆分成子组件,然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。这在一些复杂的场景之下,也能达到拆分复杂度,让每个组件更加精简的目的。

使用 render props 模式重用 UI 逻辑

render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容。

在如今的函数组件情况下,Hooks 有一个局限,那就是只能用作数据逻辑的重用,而一旦涉及 UI 表现逻辑的重用,就有些力不从心了,而这正是 render props 擅长的地方。所以,即使有了 Hooks,我们也要掌握 render props 这个设计模式的用法

import { useState, useCallback } from "react";

function CounterRenderProps({ children }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setCount(count - 1);
}, [count]);

return children({ count, increment, decrement });
}

可以看到,我们要把计数逻辑封装到一个自己不 render 任何 UI 的组件中,那么在使用的时候可以用如下的代码

function CounterRenderPropsExample() {
return (
<CounterRenderProps>
{({ count, increment, decrement }) => {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}}
</CounterRenderProps>
);
}

这里利用了 children 这个特殊属性。也就是组件开始 tag 和结束 tag 之间的内容,其实是会作为 children 属性传递给组件。那么在使用的时候,是直接传递了一个函数过去,由实现计数逻辑的组件去调用这个函数,并把相关的三个参数 count,increase 和 decrease 传递给这个函数。

当然,我们完全也可以使用其它的属性名字,而不是 children。我们只需要把这个 render 函数作为属性传递给组件就可以了,这也正是 render props 这个名字的由来。

import { useState, useCallback } from "react";

function useCounter() {
// 定义 count 这个 state 用于保存当前数值
const [count, setCount] = useState(0);
// 实现加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count]);
// 实现减 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count]);

// 将业务逻辑的操作 export 出去供调用者使用
return { count, increment, decrement };
}

很显然,使用 Hooks 的方式是更简洁的。这也是为什么我们经常说 Hooks 能够替代 render props 这个设计模式。但是,需要注意的是,Hooks 仅能替代纯数据逻辑的 render props。如果有 UI 展示的逻辑需要重用,那么我们还是必须借助于 render props 的逻辑