Recoil 中多級資料聯動及資料重置的合理做法

劉哇勇發表於2021-04-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:

相關文章