[英]How do I prevent illegal state in renders when useEffect only fires after render?
我有一個 React 組件,它接受一個 prop 並通過useState
保持狀態。 當那個道具改變時,我有時需要更新狀態作為響應,所以我添加了一個useEffect
。 但是 props 更改會導致渲染, useEffect
在渲染之后觸發,設置 state 會導致另一個render
,並且在這兩個渲染之間,我的組件處於非法狀態,我不知道如何防止它。
這是一個簡單的例子。 該組件顯示一個單選按鈕列表,每個按鈕代表一個城市。 一次僅啟用特定國家/地區內城市的單選按鈕。 當國家/地區發生變化時,它會更新啟用了哪些單選按鈕,並且還會將用戶的選擇更改為有效的城市。
import { useEffect, useState } from 'react';
const CITIES_BY_COUNTRY = {
Spain: ['Madrid', 'Barcelona', 'Valencia'],
France: ['Paris', 'Lyon', 'Marseille'],
};
export function CityPicker({ currentCountry }) {
const [selectedCity, setSelectedCity] = useState('');
// When the country changes, make sure the selected city is valid.
useEffect(() => {
if (!CITIES_BY_COUNTRY[currentCountry].includes(selectedCity)) {
setSelectedCity(CITIES_BY_COUNTRY[currentCountry][0]);
}
}, [currentCountry, selectedCity]);
// Log the country/city pair.
console.log({ currentCountry, selectedCity });
return (
<div>
{Object.keys(CITIES_BY_COUNTRY).map(country => (
<div key={`country-${country}`}>
{Object.keys(CITIES_BY_COUNTRY[country]).map(city => (
<label key={`city-${city}`}>
<input
type="radio"
name="city"
value={city}
disabled={country !== currentCountry}
checked={city === selectedCity}
onChange={() => setSelectedCity(city)}
/>
{city}
</label>
))}
</div>
))}
</div>
);
}
用戶帶着 currentCountry === "Spain" 到達。 僅啟用西班牙城市。 日志{ currentCountry: "Spain", selectedCity: "Madrid" }
用戶點擊“巴塞羅那”。 日志{ currentCountry: "Spain", selectedCity: "Barcelona" }
。 到目前為止一切都很好。
父組件中的某些內容發生了變化,並且currentCountry
更改為 France。 該組件通過新的道具並重新渲染。 日志{ currentCountry: "France", selectedCity: "Barcelona" }
。 然后, useEffect
觸發,我們得到另一個渲染。 日志{ currentCountry: "France", selectedCity: "Paris" }
。
如您所見,我們在第 3 步中得到了兩個渲染,其中一個有一個非法配對(法國 + 巴塞羅那)。
這是一個簡單的例子,我的應用程序要復雜得多。 國家和城市都可以通過多種方式進行更改,我需要每次都對這對進行驗證,有時會提示用戶或在某些情況下做出反應。 鑒於此,防止非法配對非常重要。
鑒於useEffect
只在渲染后觸發,似乎要做出我需要的更改總是為時已晚。 有沒有優雅的方法來解決這個問題?
這是我對解決方案的最佳嘗試,盡管我覺得它並不優雅。 我想知道其中一些是否可以以某種方式抽象為自定義鈎子。
export function EffectiveCityPicker({ currentCountry }) {
const [selectedCity, setSelectedCity] = useState(CITIES_BY_COUNTRY[currentCountry][0]);
// Based on the country, make sure we have a valid city.
let effectiveSelectedCity = selectedCity;
if (!CITIES_BY_COUNTRY[currentCountry].includes(selectedCity)) {
effectiveSelectedCity = CITIES_BY_COUNTRY[currentCountry][0];
}
// If the effectiveSelectedCity changes, save it back to state.
useEffect(() => {
if (selectedCity !== effectiveSelectedCity) {
setSelectedCity(effectiveSelectedCity);
}
}, [selectedCity, effectiveSelectedCity]);
// Log the country/city pair.
console.log({ currentCountry, effectiveSelectedCity });
return (
<div>
{Object.keys(CITIES_BY_COUNTRY).map(country => (
<div key={`country-${country}`}>
{Object.keys(CITIES_BY_COUNTRY[country]).map(city => (
<label key={`city-${city}`}>
<input
type="radio"
name="city"
value={city}
disabled={country !== currentCountry}
checked={city === effectiveSelectedCity}
onChange={() => setSelectedCity(city)}
/>
{city}
</label>
))}
</div>
))}
</div>
);
}
在這個版本中,我們總是將當前的 props 和 state 組合成一個effectiveSelectedCity
的 EffectiveSelectedCity ,然后在需要時將其保存回 state。 它可以防止非法對,但會導致產生相同標記的額外渲染。 如果有更多的狀態和道具,它也會變得更加復雜——也許有一個使用減速器的類似解決方案。
useMemo
在這種情況下工作,同時防止額外的渲染。 為了防止您描述的此類問題,您可以將國家和城市選擇都設為受控組件並從父級管理它們的狀態(將值和處理程序作為道具傳遞)。
import { useMemo, useState } from 'react';
const CITIES_BY_COUNTRY = {
Spain: ['Madrid', 'Barcelona', 'Valencia'],
France: ['Paris', 'Lyon', 'Marseille'],
};
export function CityPicker({ currentCountry }) {
const [selectedCity, setSelectedCity] = useState('');
const currentCity = useMemo(() => {
const defaultCity = CITIES_BY_COUNTRY[currentCountry][0];
const isCityValid = CITIES_BY_COUNTRY[currentCountry].includes(selectedCity);
return isCityValid ? selectedCity : defaultCity;
}, [selectedCity, currentCountry]);
return (
<div>
{Object.keys(CITIES_BY_COUNTRY).map(country => (
<div key={`country-${country}`}>
{Object.keys(CITIES_BY_COUNTRY[country]).map(city => (
<label key={`city-${city}`}>
<input
type="radio"
name="city"
value={city}
disabled={country !== currentCountry}
checked={city === currentCity}
onChange={() => setSelectedCity(city)}
/>
{city}
</label>
))}
</div>
))}
</div>
);
}
2022 年 6 月更新:我發現了一個使用相對隱藏的 React 功能的新解決方案。
要同步 props,你可以直接在渲染內部設置狀態,一旦你的渲染返回,React 就會更新狀態並重新渲染! 這不僅在官方 React 文檔中受到鼓勵,而且與我的原始文檔類似的示例被呈現為“不該做什么”。
考慮到這一點,這是我的新解決方案:
import { useState } from 'react';
const CITIES_BY_COUNTRY = {
Spain: ['Madrid', 'Barcelona', 'Valencia'],
France: ['Paris', 'Lyon', 'Marseille'],
};
export function CityPicker({ currentCountry }) {
const [selectedCity, setSelectedCity] = useState('');
// When the country changes, make sure the selected city is valid.
// NOTE: No useEffect here. React will re-render immediately after
// first render.
if (!CITIES_BY_COUNTRY[currentCountry].includes(selectedCity)) {
setSelectedCity(CITIES_BY_COUNTRY[currentCountry][0]);
}
return (
<div>
{Object.keys(CITIES_BY_COUNTRY).map(country => (
<div key={`country-${country}`}>
{Object.keys(CITIES_BY_COUNTRY[country]).map(city => (
<label key={`city-${city}`}>
<input
type="radio"
name="city"
value={city}
disabled={country !== currentCountry}
checked={city === selectedCity}
onChange={() => setSelectedCity(city)}
/>
{city}
</label>
))}
</div>
))}
</div>
);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.