簡體   English   中英

當 useEffect 僅在渲染后觸發時,如何防止渲染中的非法狀態?

[英]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>
  );
}
  1. 用戶帶着 currentCountry === "Spain" 到達。 僅啟用西班牙城市。 日志{ currentCountry: "Spain", selectedCity: "Madrid" }

  2. 用戶點擊“巴塞羅那”。 日志{ currentCountry: "Spain", selectedCity: "Barcelona" } 到目前為止一切都很好。

  3. 父組件中的某些內容發生了變化,並且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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM