• Recoil 中多级数据联动及数据重置的合理做法


    前情回顾

    书接上回,前面引出了在数据存在级联的情况下,各下拉框之间的默认值及值变化的处理。简单回顾一下:

    场景是:

    • 地域下拉决定可选的可用区
    • 默认选中第一个地域,通过设置 atomdefault 字段
    • 默认选中该地域下第一个可用区,通过设置 atomdefault 字段

    问题:

    • 手动选择一下可用区,此时更新了可用区的值
    • 手动选择一下地域,此时更新了地域,可用区下拉框同步更新,此时实际可用区的值为前面手动选择的旧值,界面上却展示的新可用区的第一个。

    解决:

    • 在地域选择组件中,当地域发生变化时,重置一下可用区使其回到默认值。

    新的问题

    进一步实践,会发现这种解决方式存在缺陷,在多级级联的情况下,比如三个下拉框 A->B->C,A 决定 B, B 决定 C,按照这个解决思路,

    • 在 A 变化时需要重置 B,C
    • B 变化时需要重置 C

    这显然不科学,非常冗余。同时从组件解耦的角度来看,A,B 需要知道谁依赖了自己从而重置它们,这种耦合非常难以维护。

    因此应该反过来,将解决问题的逻辑囿于组件自身才是科学的做法。

    于是 A 不管其他,只管自己随便随便怎么变化,B 中监听 A 变化然后做出反应以重置自己,C 监听 B 的变化以重置自己。这样逻辑做到了内聚无耦合。

    而之前文章中之所以没用这种方式,是因为发现该方式具有滞后性,组件内部会停留在错误的值上渲染一次。

    export function ZoneSelect() {
    + const region = useRecoilValue(regionState);
      const zones = useRecoilValue(zonesState);
      const [zone, setZone] = useRecoilState(zoneState);
    
    + console.log("zone:", zone.id);
    
    + useEffect(() => {
    +   setZone(zones[0]);
    + }, [region]);
    
      return (
        <label htmlFor="zoneId">
         …
        </label>
      );
    }

    这里会先打印一次旧值,等 useEffect 执行完后才会打印正确的值。如果在旧值的情形下依赖该状态去做了些业务逻辑,势必会导致错误,比如拿这个旧值去发起请求。

    状态的正确使用

    细思会发现,上面之所以会有这种错误是因为姿势没对,假若我们要使用可用区的值,应该在 useEffect 中进行,亦即:

      useEffect(() => {
        // do sth with zone
        console.log("zone", zone.id);
      }, [zone]);

    此时打印就会得到正确的结果。

    按照这个逻辑修正后的组件及联动关系就成了:

    RegionSelect.tsx

    export function RegionSelect() {
      const regions = useRecoilValue(regionsState);
      const [region, setRegion] = useRecoilState(regionState);
    
      return (
        <label htmlFor="regionId">
          Region:
          <select
            name="regionId"
            id="regionId"
            value={region.id}
            onChange={(event) => {
              const regionId = event.target.value;
              const region = regions.find((region) => region.id === regionId);
              setRegion(region!);
            }}
          >
            {regions.map((region) => (
              <option key={region.id} value={region.id}>
                {region.id}
              </option>
            ))}
          </select>
        </label>
      );
    }

    ZoneSelect.tsx

    export function ZoneSelect() {
      const zones = useRecoilValue(zonesState);
      const [zone, setZone] = useRecoilState(zoneState);
      const resetZone = useResetRecoilState(zoneState);
      const region = useRecoilValue(regionState);
    
      // region 变化后重置 zone
      useEffect(() => {
        resetZone();
      }, [region, resetZone]);
    
      useEffect(() => {
        // do sth with zone
        console.log("zone", zone.id);
      }, [zone]);
    
      return (
        <label htmlFor="zoneId">
          Zone:
          <select
            name="zoneId"
            id="zoneId"
            value={zone.id}
            onChange={(event) => {
              const zoneId = event.target.value;
              const zone = zones.find((zone) => zone.id === zoneId);
              setZone(zone!);
            }}
          >
            {zones.map((zone) => (
              <option key={zone.id} value={zone.id}>
                {zone.id}
              </option>
            ))}
          </select>
        </label>
      );
    }

    优化数据的依赖关系

    进一步思考,导致可用区需要重置的直接原因其实并不是地域发生了变化,而是地域发生变化后,可用区下拉框的可选项发生了变化,亦即 zonesState。既然下拉选项变化了,当然需要重置默认值为新的下拉选项中的第一个。所以可用区组件中直接监听下拉选项,而非地域。

    export function ZoneSelect() {
      const zones = useRecoilValue(zonesState);
      const [zone, setZone] = useRecoilState(zoneState);
      const resetZone = useResetRecoilState(zoneState);
    
      useEffect(() => {
        resetZone();
      }, [resetZone, zones]);
    
      useEffect(() => {
        // do sth with zone
        console.log("zone", zone.id);
      }, [zone]);
    
      return (
        <label htmlFor="zoneId">
          Zone:
          <select
            name="zoneId"
            id="zoneId"
            value={zone.id}
            onChange={(event) => {
              const zoneId = event.target.value;
              const zone = zones.find((zone) => zone.id === zoneId);
              setZone(zone!);
            }}
          >
            {zones.map((zone) => (
              <option key={zone.id} value={zone.id}>
                {zone.id}
              </option>
            ))}
          </select>
        </label>
      );
    }

    这样一来,组件内部就清爽多了,只有自身相关的数据,甚至都去掉了对 regionState 的使用。

    selector 派生数据的隐形桥梁功能

    这里其实是 zonesState 作为桥梁自动完成了对 region 的监听,因为 zonesStateselector,它是从 regionState 派生出来的数据,在 regionState 发生变化时,会由 Recoil 负责更新。

    其他

    最后,示例代码参见 wayou/recoil-nest-select

    The text was updated successfully, but these errors were encountered:

    CC BY-NC-SA 署名-非商业性使用-相同方式共享
  • 相关阅读:
    卷积神经网络入门(1) 识别猫狗
    lumen 获得当前uri 如/xxx/{id}
    React ES5 (createClass) 和 ES6 (class)
    lumen 单元测试
    mysql 高级语法手记
    react手记(componentWillMount,componentDidMount等)
    lumen 事件
    PDO drivers no value in Windows
    BindingNavigator操作DatagridView的数据
    <input type="hidden" id="haha" name="wang" value="xiaodong" />
  • 原文地址:https://www.cnblogs.com/Wayou/p/14649508.html
Copyright © 2020-2023  润新知