-
Notifications
You must be signed in to change notification settings - Fork 0
Description
开始前请先阅读 👉 Adjusting some state when a prop changes · React Docs Beta
本文默认你熟悉 React Hooks,如果不了解可以查看 React Hooks 文档。
======== 以下是正文 ========
考虑这样一个场景,有一个列表,你可以通过接口得到筛选后的数量 filteredCount 和未加筛选的数量 allCount;有一个导出按钮,点击后打开一个弹窗,提示你可以选择导出的方式,点击即发起导出的请求,就像这样:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ 导出为 Excel ×
┠───────────────────────────────────
┃
┃ ⦿ 筛选结果(共 567 条数据)
┃ ◯ 全部结果(共 765 条数据)
┃
┠───────────────────────────────────
┃ 取消 | 导出
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
模版代码
const ExportType = {
Filtered = 1,
All = 2,
};
const MyModal = props => {
// #props
const { visible, filteredCount, allCount } = props;
// #state
const [exportType, setExportType] = useState(ExportType.Filtered);
// #handler
const handleSubmit = () => {
request('/api/export', { exportType });
};
const handleRadioChange = e => {
setExportType(e.target.value);
};
// #render
return (
<Modal
visible={visible}
onOk={handleSubmit}
>
<Radio.Group
value={exportType}
onChange={handleRadioChange}
>
<Radio value={ExportType.Filtered}>
Export filtered result ({filteredCount} records)
</Radio>
<Radio value={ExportType.All}>
Export all result ({allCount} records)
</Radio>
</Radio.Group>
</Modal>
);
};现在分别有两个需求要实现:
- 如果筛选结果的数量为 0,则禁用掉“筛选结果” Radio,并且自动选上“全部结果” Radio;
- 重新呼出弹窗都需要默认选择“筛选结果” Radio,特殊情况如果打开时筛选结果的数量为 0,按照 1 的方式处理;
在这个例子中,可能变化的 props 有 visible 和 filteredCount,内部的 state exportType 需要随之改变而做调整(adjust)。下面就用两种实现方式,作为例子来解释 You-Might-Not-Need-an-Effect 中关于「Adjusting some state when a prop changes」这个场景的建议。
Effect 实现
首先考虑第二个需求,我会很习惯性地想到,“关闭弹窗”就是 visible 变为 false 嘛,那就用 useEffect “监听”一下就完事了;同时考虑第一个需求,我可以在“componentDidMount / componentDidUpdate”那样的时机,判断一遍 filteredCount 的值,然后进行 setExportType 就可以了,于是乎代码如下:
// #props
// ...
const isFilteredEmpty = filteredCount <= 0;
// #state
// ...
// #effect
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
if (visible) {
setExportType(
isFilteredEmpty ? ExportType.All : ExportType.Filtered
);
}
}, [visible, isFilteredEmpty]);
// #handler
// ...
// #render
return (
<Modal {/** ... */}>
<Radio.Group {/** ... */}>
<Radio value={ExportType.Filtered} disabled={isFilteredEmpty}>
Export filtered result ({filteredCount} records)
</Radio>
<Radio value={ExportType.All}>
Export all result ({allCount} records)
</Radio>
</Radio.Group>
</Modal>
);这么写有几个问题:
- 很明显违背了 Effect 作为逃生舱的设计初衷,并没有实现“与外部系统,如非 React 窗体、网络(理解为 API)或浏览器 DOM,进行同步”的效果
- 简单地将 Effect 用作 componentDidMount / componentDidUpdate 这样的生命周期钩子,但显然二者是有区别的
从字面上来理解,由于 props 变化造成的组件内部 state 的调整,不能算是一种“副作用”,它完全是 React 自身可控的,所以这里其实完全没有必要请到 useEffect 这尊大佛。下面用文档推荐的方式来解决这个问题。
最佳实践 | Best Practice
首先考虑第一个问题。可以看到 state exportType 依赖于 prop filteredCount 的变化,那不就是 useMemo 包一下嘛,但是这个 state 还可以由用户通过 UI 来改变,也就是同时具备 setState 能力的一个新 Hook —— “useMemoState”。
现在我们抛开 Hook 想想,一定要是 “useMemo” 吗?由 prop filteredCount 的 state exportType 的计算并不是昂贵的(expensive),我们直接在 render 阶段将这个值计算出来就可以了:
// #state
// ✅ Best: Calculate everything during rendering
const [exportType, setExportType] = useState(ExportType.Filtered);
const _exportType = isFilteredEmpty ? ExportType.All : exportType;然后,最关键的一点,现在我们认为这个计算得到的值就是一个 state,我们只要把它改个名,就和原来的 exportType 一样的用法,它既作为 Radio.Group 的 value 传参,也作为 handleSubmit 中 API 的参数:
// #state
// ✅ Best: Calculate everything during rendering
const [_exportType, setExportType] = useState(ExportType.Filtered);
const exportType = isFilteredEmpty ? ExportType.All : exportType;这样我们避免了用 Effect 来实现第一个需求。接下来看第二个:“重新呼出弹窗”的时候默认选中“筛选结果”。
在 Effect 实现中,我们企图在“重新呼出弹窗”的时候,即 visible=true 的情况赋值,现在转变一下思路,在“关闭弹窗”的时候做这件事是不是一样的效果?反正用户关闭弹窗的时候也大概率看不到瞬时的变化,相当于关闭弹窗就进行了一次“复位”。
说到“复位”,可能又想到在 useEffect 中返回一个“用于清除订阅器的回调”这个例子,但是这里我们并没有“订阅”什么东西,所以这么实现也是不妥的,恰好相反,它是一个用户交互事件(Event),也就是说“复位”这件事我们需要在“关闭弹窗”这个事件的 handler 中去做,比如 AntD 的 Modal 的话 afterClose 就是这样一个回调,于是代码调整如下:
// #handler
// ...
const handleModalClose = () => {
setExportType(ExportType.Filtered);
};
// ...
// #render
return (
<Modal afterClose={handleModalClose} {/** ... */}>
{/** ... */}
</Modal>
);验证
最后用这几个用例来测试一下逻辑的正确性
注:方便起见
- prop 中
visble简写为v,filteredCount简写为f- state 中
_exportType简写为_e,exportType简写为e1是ExportType.Filtered的值,2是ExportType.All的值
| 步骤 | 说明 | Prop | State | 效果 |
|---|---|---|---|---|
| 1 | 初始状态, 筛选结果数量为 100 |
v: false f: 100 |
_e: 1 e: 1 |
弹窗隐藏中 |
| 验证需求 2 | ||||
| 2 | 呼出弹窗 | v: true f: 100 |
_e: 1 e: 1 |
弹窗显示 选中“筛选结果” |
| 3 | 选中“全部结果” | v: true f: 100 |
_e: 2 e: 2 |
弹窗显示 选中“全部结果” |
| 4 | 关闭弹窗,重新呼出 | v: true f: 100 |
_e: 1 e: 1 |
弹窗显示 选中“筛选结果” |
| 验证需求 1 | ||||
| 5 | 改变筛选条件,使得结果为空, 此时呼出弹窗 |
v: true f: 0 |
_e: 1 e: 2 |
弹窗显示 选中“全部结果” |
| 6 | 关闭弹窗,重新呼出 | v: true f: 0 |
_e: 1 e: 2 |
弹窗显示 选中“全部结果” |
| 7 | 关闭弹窗 改变筛选条件,使得结果非空 此时呼出弹窗 |
v: true f: 200 |
_e: 1 e: 1 |
弹窗显示 选中“筛选结果” |
总结
对于「Adjusting some state when a prop changes」这种场景,用文档中的一句注释来描述就是:
Calculate everything during rendering,把能做的计算都放到 render 阶段来做
以此为基础,还要弄清楚是究竟是 prop 变化导致 state 要跟着变,还是“Event”导致的,如果是后者,那就在 handler 里 adjust。
这样,就可以不用 Effect 也能完美解决。