Recoil 預設值及資料級聯的使用

劉哇勇發表於2021-04-09

Recoil 中預設值及資料間的依賴

通過 Atom 可方便地設定資料的預設值,

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

而 Selector 可方便地設定資料的級聯依賴關係,即,另一個資料可從現有資料進行派生。

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

結合這兩個特點,在實現資料間存在聯動的表單時,非常方便。

一個實際的例子

考察這樣的場景,購買雲資源時,會先選擇地域,根據所選地域再選擇該地域下的可用區。

這裡就存在設定預設值的問題,未選擇時自動選中預設地域及對應地域下的預設可用區,也涉及資料間的級聯依賴,可選的可用區要根據地域而變化。

呈現的效果如下:


image

地域及可用區的選擇

實現地域及可用區的選擇

下面就通過 Recoil 來實現上述地域及可用區的選擇邏輯。

建立示例專案

$  yarn create react-app recoil-nest-select --template typescript

新增並使用 Recoil

安裝依賴:

$ yarn add recoil

使用 Recoil, 首先將應用包裹在 RecoilRoot 中:

index.tsx

import { RecoilRoot } from "recoil";

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <Suspense fallback="loading...">
        <App />
      </Suspense>
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

新增 appState.ts 檔案存放 Recoil 狀態資料,目前先定義好地域和可用區的型別,

appState.ts

interface IZone {
  id: string;
  name: string;
}

interface IRegion {
  id: string;
  name: string;
  zones: IZone[];
}

新增假資料

根據上面定義的型別,新增假資料:

mock.ts

export const mockRegionData = [
  {
    id: "beijing",
    name: "北京",
    zones: [
      {
        id: "beijing-zone-1",
        name: "北京一區",
      },
      {
        id: "beijing-zone-2",
        name: "北京二區",
      },
      {
        id: "beijing-zone-3",
        name: "北京三區",
      },
    ],
  },
  {
    id: "shanghai",
    name: "上海",
    zones: [
      {
        id: "shanghai-zone-1",
        name: "上海一區",
      },
      {
        id: "shanghai-zone-2",
        name: "上海二區",
      },
      {
        id: "shanghai-zone-3",
        name: "上海三區",
      },
    ],
  },
  {
    id: "guangzhou",
    name: "廣州",
    zones: [
      {
        id: "guangzhou-zone-1",
        name: "廣州一區",
      },
      {
        id: "guangzhou-zone-2",
        name: "廣州二區",
      },
    ],
  },
];

新增狀態資料

新增地域及可用區狀態資料,先看地域資料,該資料用來生成地域的下拉框。真實情況下,該資料來自非同步請求,這裡通過 Promise 模擬非同步資料。

appState.ts

import { atom, selector } from "recoil";
import { mockRegionData } from "./mock";

export const regionsState = selector({
  key: "regionsState",
  get: ({ get }) => {
    return Promise.resolve<IRegion[]>(mockRegionData);
  },
});

新增一個狀態用於儲存當前選中的地域:

appState.ts

export const regionState = atom({
  key: "regionState",
  default: selector({
    key: "regionState/Default",
    get: ({ get }) => {
      const regions = get(regionsState);
      return regions[0];
    },
  }),
});

這裡通過使用 atom 並指定預設值為地域第一個資料,達到下拉框預設選中第一個的目的。

新增地域選擇元件

新增地域選擇元件,使用上面建立的地域資料。

RegionSelect.tsx

import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { regionsState, regionState } from "./appState";

export function RegionSelect() {
  const regions = useRecoilValue(regionsState);
  const [region, setRegion] = useRecoilState(regionState);
  return (
    <label htmlFor="regionId">
      地域:
      <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.name}
          </option>
        ))}
      </select>
    </label>
  );
}

至此地域部分完成,可用區同理,只不過可用區的拉下資料依賴於當前選中的地域。

新增可用區狀態資料及下拉元件

appState.tsx

export const zonesState = selector({
  key: "zonesState",
  get: ({ get }) => {
    const region = get(regionState);
    return region.zones;
  },
});

export const zoneState = atom({
  key: "zoneState",
  default: selector({
    key: "zoneState/default",
    get: ({ get }) => {
      return get(zonesState)[0];
    },
  }),
});

可選擇的可用區依賴於當前選中的地域,通過 const region = get(regionState); 實現獲取到當前選中地域的目的。

可用區的預設值也是拿到當前可選的所有地域,然後取第一個,return get(zonesState)[0];

ZoneSelect.tsx

import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { zonesState, zoneState } from "./appState";

export function ZoneSelect() {
  const zones = useRecoilValue(zonesState);
  const [zone, setZone] = useRecoilState(zoneState);
  return (
    <label htmlFor="zoneId">
      可用區:
      <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.name}
          </option>
        ))}
      </select>
    </label>
  );
}

展示當前地域及可用區

將前面兩個下拉框展示出來,同時展示當前地域及可用區。

App.tsx

import React from "react";
import { useRecoilValue } from "recoil";
import "./App.css";
import { regionState, zoneState } from "./appState";
import { RegionSelect } from "./RegionSelect";
import { ZoneSelect } from "./ZoneSelect";

function App() {
  const region = useRecoilValue(regionState);
  const zone = useRecoilValue(zoneState);
  return (
    <div className="App">
      <p>region:{region.name}</p>
      <p>zone:{zone.name}</p>
      <RegionSelect />
      <ZoneSelect />
    </div>
  );
}

export default App;

至此完成了整個程式的實現。

最終效果

來看看效果:

Screen Recording 2021-02-19 at 9 17 55 PM mov

地域及可用區聯動效果

帶預設值的狀態未自動更新的問題

上面的實現乍一看實現了功能,但進行可用區的選擇之後問題便會暴露。

Screen Recording 2021-02-19 at 9 25 19 PM mov

可用區未聯動的問題

可以看到可用區更新後,再切換地域,雖然下拉框中可選的可用區更新了,但實際上當前可用區的值停留在了上一次選中的值,並沒有與地域聯動。如果不是把可用區展示出來,不容易發現這裡的問題,具有一定迷惑性。

看看可用區下拉值 zones 的來源不難發現,

export const zonesState = selector({
  key: "zonesState",
  get: ({ get }) => {
    const region = get(regionState);
    return region.zones;
  },
});

因為可用區是從當前選中的地域資料 regionState 中獲取的,當變更地域後,regionState 更新,導致 zonesState 更新,所以下拉框能正確同步,沒問題。

再看看當前選中的可用區 zoneState

export const zoneState = atom({
  key: "zoneState",
  default: selector({
    key: "zoneState/default",
    get: ({ get }) => {
      return get(zonesState)[0];
    },
  }),
});

它通過 atom 承載,同時指定了預設值,為 zonesState 中第一個資料。

當切換地域時,zonesState 確實更新了,進而 zoneState 的預設值也會重新獲取,所以始終會預設選中第一個可用區。

當我們手動進行了可用區選擇時,在可用區下拉元件中,

      <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.name}
          </option>
        ))}
      </select>

onChange 事件的回撥中通過 setZone 更新了 zoneState,此時可用區 zoneState 已經有一個人為設定的值,預設值就不起作用了,因此在切換地域後,zoneState 仍為這裡 onChange 設定的值。

手動新增依賴

直接的修復方式可以在可用區元件中監聽地域的變化,當地域變化後,設定一次可用區。

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 執行完畢後,才列印出正確的值,即,這種方式的修復,有滯後性。

Screen Recording 2021-02-20 at 10 55 28 AM mov

通過 `useEffect` 方式來修正,可用區更新會滯後

useResetRecoilState

查閱 Recoil 文件,發現 useResetRecoilState 可用於重置狀態到預設值。

這裡的思路可以是,在地域變化後,重置一下可用區,這樣之前手動選擇的值便失效,可用區恢復到預設狀態。

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

這裡 resetZonesetRegion 的順序不影響,都能達到目的。

Screen Recording 2021-02-20 at 11 15 56 AM mov

通過 `useResetRecoilState` 重置狀態到預設值

通過列印的值來看,一切正常,問題得以修正。

相關資源

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

相關文章