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

  • 2021 年 4 月 12 日
  • 筆記

前情回顾

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

场景是:

  • 地域下拉决定可选的可用区
  • 默认选中第一个地域,通过设置 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: