Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wayou opened this issue Feb 24, 2021 · 0 comments
Open

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

wayou opened this issue Feb 24, 2021 · 0 comments

Comments

@wayou
Copy link
Owner

wayou commented Feb 24, 2021

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

@wayou wayou changed the title Recoil 中多级数组联动及数据重置的合理做法 Recoil 中多级数据联动及数据重置的合理做法 Feb 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant