Recoil 默认值及数据级联的使用
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}`;
},
});
结合这两个特点,在实现数据间存在联动的表单时,非常方便。
一个实际的例子
考察这样的场景,购买云资源时,会先选择地域,根据所选地域再选择该地域下的可用区。
这里就存在设置默认值的问题,未选择时自动选中默认地域及对应地域下的默认可用区,也涉及数据间的级联依赖,可选的可用区要根据地域而变化。
呈现的效果如下:

地域及可用区的选择
实现地域及可用区的选择
下面就通过 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;
至此完成了整个程序的实现。
最终效果
来看看效果:
地域及可用区联动效果
带默认值的状态未自动更新的问题
上面的实现乍一看实现了功能,但进行可用区的选择之后问题便会暴露。

可用区未联动的问题
可以看到可用区更新后,再切换地域,虽然下拉框中可选的可用区更新了,但实际上当前可用区的值停留在了上一次选中的值,并没有与地域联动。如果不是把可用区展示出来,不容易发现这里的问题,具有一定迷惑性。
看看可用区下拉值 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
执行完毕后,才打印出正确的值,即,这种方式的修复,有滞后性。

通过 `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>
);
}
这里 resetZone
和 setRegion
的顺序不影响,都能达到目的。

通过 `useResetRecoilState` 重置状态到默认值
通过打印的值来看,一切正常,问题得以修正。