背景
最近接到一个需求,大概是对一些稿子操作分散在各个页面的表单,需要来回操作,效率低,希望可以通过将这些运营操作,放入一个表单集中提交管理和维护,大致实现如下demo:
业务逻辑
这里定义两个自定义表单组件: 运营动作:TagButton
,相关文章:ArticleFormList
从上图可以看到他们是有联动关系的:
TagButton
高亮选中时,至少存在一篇文章是对应运营动作;TagButton
手动点击高亮时,相当于全选按钮,高亮所有文章对应运营动作;ArticleFormList
列表第一次选中某项运营动作,对应TagButton
按钮应该高亮,表示这些稿子存在这些运营动作;ArticleFormList
列表从存在有某项运营动作,到全都没有,应该取消TagButton
按钮高亮,表示这些稿子没有这种运营动作;
当然实际需求还要比这些更复杂 ╮( ̄▽ ̄)╭
思考
说一下思考: 这个需求有两块代码实现逻辑:
- 如何实现表单自定义受控组件;
- 它们之间联动关系应该写在哪里比较合适;
1. 自定义受控组件写成既受,又非控组件
对(1)来说:不要将自定义受控组件写成既受,又非控组件:笔者认为应该用父级传入的value
和onChange
去驱动组件值和改变,不要再定义额外的state
值,这样会额外去对内部状态state
值,与表单formStore value
值做双向绑定是一种既受,又非控的状态;
受控组件(听话的孩子):内部状态和改变状态函数由父组件提供,受父组件控制,自身不维护状态改变。
非受控组件(独立的孩子):内部状态值与父级组件隔离,父级组件无法插手干预孩子状态改变,由孩子自己管理自身状态。
表单中既受,又非控的写法是有问题的,很明显,表单项的值最后都交到formStore
手里才能提交表单,你只需它给你value
值,子组件去渲染,子组件事件回调发生变化通知formStore
,onChange
改变值。所以需要定义成受控组件。
2. 不要滥用useEffect
,去做响应式联动逻辑
对(2)来说:. 不要滥用useEffect
,去做响应式联动逻辑:这一点其实在react官网也提到了。可以看这篇;
react官网介绍:如何移除不必要的 Effect
有两种不必使用 Effect 的常见情况:
1.你不必使用 Effect 来转换渲染所需的数据。例如,你想在展示一个列表前先做筛选。你的直觉可能是写一个当列表变化时更新 state 变量的 Effect。然而,这是低效的。当你更新这个 state 时,React 首先会调用你的组件函数来计算应该显示在屏幕上的内容。然后 React 会把这些变化“提交”到 DOM 中来更新屏幕。然后 React 会执行你的 Effect。如果你的 Effect 也立即更新了这个 state,就会重新执行整个流程。为了避免不必要的渲染流程,应在你的组件顶层转换数据。这些代码会在你的 props 或 state 变化时自动重新执行。
State 变化 => 计算屏幕显示内容=> "提交"DOM到浏览器渲染引擎更新屏幕=>React执行Effect=>又更新state=> .....重新执行整个流程。应该在组件顶层转换数据,传入props或state变化会自动重新执行。
2. 你不必使用 Effect 来处理用户事件。例如,你想在用户购买一个产品时发送一个
/api/buy
的 POST 请求并展示一个提示。在这个购买按钮的点击事件处理函数中,你确切地知道会发生什么。但是当一个 Effect 运行时,你却不知道用户做了什么(例如,点击了哪个按钮)。这就是为什么你通常应该在相应的事件处理函数中处理用户事件。你 的确 可以使用 Effect 来和外部系统 同步 。例如,你可以写一个 Effect 来保持一个 jQuery 的组件和 React state 之间的同步。你也可以使用 Effect 来获取数据:例如,你可以同步当前的查询搜索和查询结果。请记住,比起直接在你的组件中写 Effect,现代 框架 提供了更加高效的,内置的数据获取机制。
用useEffect
前,先考虑是否可以使用事件回调函数,现代框架提供了更加高效的,内置的数据获取机制。
核心代码
1. TagButton 错误代码示例:
- 接收父级
value
和onChange
同时,内部又去定义自身状态checkedValueList
,然后再用useEffect
对formStore`` value
和checkedValueList
状态,做一个双向关联绑定; - 用
useWatch
和useEffect
去做表单组件之间响应联动逻辑;
// TagButton 错误代码示例:
const TagButton = ({ value = [], onChange })=> {
const [checkedValueList, setCheckedValueList] = useState([]);
const form = Form.useFormInstance();
const ArticleFormList = Form.useWatch('articleList', form);
// 内容状态变化,同步到表单
useEffect(() => {
onChange && onChange(checkedValueList);
// eslint-disable-next-line
}, [checkedValueList]);
// 表单值变化,同步到内部状态
useEffect(() => {
// 表单值和内部状态不同才修改内部状态
if (Array.isArray(value) && !isEqual(value, checkedValueList)) {
setCheckedValueList(value);
}
// eslint-disable-next-line
}, [value]);
// 响应联动,勾选和取消勾选
useEffect(() => {
// 实现响应联动逻辑
...
}, [ArticleFormList]);
....
}
2. TagButton 正确代码示例:
value
和onChange
直接绑定到组件值和事件回调上面,- 响应联动逻辑,可以直接在事件回调函数处理,直接在手动触发回调函数处理即可,能不使用
useEffect
就不用用它,万不得已才请它出来 (`Д´*)。
const TagButton: FC<TagButtonProps> = ({
value: currentSelectedList = [],
onChange,
}) => {
const form = Form.useFormInstance();
const handleOnChangeCheck = (targetValue: ActionType) => {
// 实现响应联动逻辑
...
onChange && onChange(result);
};
return (
<div>
{actionTypeList.map((item, index) => {
const currentValue = item.value;
const isCheck = currentSelectedList.includes(currentValue);
return (
<Tag.CheckableTag
key={index}
onChange={() => handleOnChangeCheck(currentValue)}
checked={isCheck}
>
{item.label}
</Tag.CheckableTag>
);
})}
</div>
);
};
总结
很明显错误代码示,用了很多useEffect
去实现状态同步更新,获取数据操作,但实际上这些操作是不必要的,阅读起来比较费力,如果你直接写在相关事件处理函数中,你是明确用户处理操作,但当你写在useEffect
时,你读代码不清楚用户做了什么,(点了那个按钮)。笔者借用官网原话,对useEffect介绍:
Effect 是 React 范式中的一种脱围机制。它们让你可以 “逃出” React 并使组件和一些外部系统同步,比如非 React 组件、网络和浏览器 DOM。如果没有涉及到外部系统(例如,你想根据 props 或 state 的变化来更新一个组件的 state),你就不应该使用 Effect。移除不必要的 Effect 可以让你的代码更容易理解,运行得更快,并且更少出错。