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 默认值及数据级联的使用 #264

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

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

wayou opened this issue Feb 20, 2021 · 0 comments

Comments

@wayou
Copy link
Owner

wayou commented Feb 20, 2021

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

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

相关资源

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