Recoil 默认值及数据级联的使用

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` 重置状态到默认值

通过打印的值来看,一切正常,问题得以修正。

相关资源