前情回顧
書接上回,前面引出了在數據存在級聯的情況下,各下拉框之間的默認值及值變化的處理。簡單回顧一下:
場景是:
- 地域下拉決定可選的可用區
- 默認選中第一個地域,通過設置
atom
的default
字段 - 默認選中該地域下第一個可用區,通過設置
atom
的default
字段
問題:
- 手動選擇一下可用區,此時更新了可用區的值
- 手動選擇一下地域,此時更新了地域,可用區下拉框同步更新,此時實際可用區的值為前面手動選擇的舊值,界面上卻展示的新可用區的第一個。
解決:
- 在地域選擇組件中,當地域發生變化時,重置一下可用區使其回到默認值。
新的問題
進一步實踐,會發現這種解決方式存在缺陷,在多級級聯的情況下,比如三個下拉框 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
的監聽,因為 zonesState
是 selector
,它是從 regionState
派生出來的數據,在 regionState
發生變化時,會由 Recoil 負責更新。
其他
最后,示例代碼參見 wayou/recoil-nest-select。
The text was updated successfully, but these errors were encountered: