(需求经验总结篇)——实现一个自定义表单受控组件,你真的需要useEffect吗?

222 阅读6分钟

背景

最近接到一个需求,大概是对一些稿子操作分散在各个页面的表单,需要来回操作,效率低,希望可以通过将这些运营操作,放入一个表单集中提交管理和维护,大致实现如下demo:

20240324-131946.gif 完整示例代码demo入口

业务逻辑

这里定义两个自定义表单组件: 运营动作:TagButton,相关文章:ArticleFormList 从上图可以看到他们是有联动关系的:

  1. TagButton 高亮选中时,至少存在一篇文章是对应运营动作;
  2. TagButton手动点击高亮时,相当于全选按钮,高亮所有文章对应运营动作;
  3. ArticleFormList 列表第一次选中某项运营动作,对应TagButton按钮应该高亮,表示这些稿子存在这些运营动作;
  4. ArticleFormList 列表从存在有某项运营动作,到全都没有,应该取消TagButton按钮高亮,表示这些稿子没有这种运营动作;

当然实际需求还要比这些更复杂 ╮( ̄▽ ̄)╭

思考

说一下思考: 这个需求有两块代码实现逻辑:

  1. 如何实现表单自定义受控组件;
  2. 它们之间联动关系应该写在哪里比较合适;

1. 自定义受控组件写成既受,又非控组件

对(1)来说:不要将自定义受控组件写成既受,又非控组件:笔者认为应该用父级传入的valueonChange 去驱动组件值和改变,不要再定义额外的state值,这样会额外去对内部状态state值,与表单formStore value值做双向绑定是一种既受,又非控的状态;

受控组件(听话的孩子):内部状态和改变状态函数由父组件提供,受父组件控制,自身不维护状态改变。

非受控组件(独立的孩子):内部状态值与父级组件隔离,父级组件无法插手干预孩子状态改变,由孩子自己管理自身状态。

表单中既受,又非控的写法是有问题的,很明显,表单项的值最后都交到formStore手里才能提交表单,你只需它给你value值,子组件去渲染,子组件事件回调发生变化通知formStoreonChange改变值。所以需要定义成受控组件。

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 错误代码示例

  1. 接收父级 valueonChange同时,内部又去定义自身状态checkedValueList,然后再用useEffectformStore`` valuecheckedValueList状态,做一个双向关联绑定;
  2. useWatchuseEffect 去做表单组件之间响应联动逻辑;
 // 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 正确代码示例

  1. valueonChange 直接绑定到组件值和事件回调上面,
  2. 响应联动逻辑,可以直接在事件回调函数处理,直接在手动触发回调函数处理即可,能不使用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 可以让你的代码更容易理解,运行得更快,并且更少出错。