Реагируйте на выбор раскрывающихся списков, которые зависят друг от друга

Я пытаюсь сделать 3 раскрывающихся списка выбора автоматически меняющимися в зависимости от выбора. Первый раскрывающийся список не имеет зависимостей, второй зависит от первого, а третий зависит от второго.

Это очень упрощенная версия моего кода:

// record, onSave, planets, countries, cities passed as props

const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
const [selectedCountry, setSelectedCountry] = useState(record.country);
const [selectedCity, setSelectedCity] = useState(record.city);

const filteredCountries = countries.filter(c => c.planet === selectedPlanet.id);
const filteredCities = cities.filter(c => c.country === selectedCountry.id);

return (
  <div>
    <select value = {selectedPlanet.id} onChange = {(e) => setSelectedPlanet(e.target.value)}>
      {planets.map(p => (
        <option key = {p.id} value = {p.id} name = {p.name} />
      )}
    </select>

    <select value = {selectedCountry.id} onChange = {(e) => setSelectedCountry(e.target.value)}>
      {filteredCountries.map(c => (
        <option key = {c.id} value = {c.id} name = {c.name} />
      )}
    </select>


    <select value = {selectedCity.id} onChange = {(e) => setSelectedCity(e.target.value)}>
      {filteredCities.map(c => (
        <option key = {c.id} value = {c.id} name = {c.name} />
      )}
    </select>

    <button onClick = {() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity ) }
  </div>
);

Параметры выбора будут обновлены соответствующим образом, но onSave() получит устаревшие значения для страны и города, если я выберу планету и нажму кнопку «Сохранить».

Это потому, что setSelectedCountry и setSelectedCity не вызываются при смене планеты. Я знаю, что могу просто вызвать их в этом случае, но это сделало бы код намного уродливее, потому что мне пришлось бы дублировать фильтрацию по стране и городу. Есть ли лучший способ обойти это?

почему страны и города не фильтруются снова каждый раз при рендере?

Frazer Kirkman 15.12.2022 05:41

использование хука useMemo решит вашу проблему. вы можете найти код в моем сообщении

Xiao Wang 20.12.2022 02:27
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
5
2
291
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

Обновлять

Я обновил пример кода для подхода useReducer. Функционально он эквивалентен исходному примеру, но, надеюсь, с более чистой логикой и структурой.

Живая демонстрация обновленного примера: stackblitz

Хотя для этого требуется дополнительная проводка, useReducer можно было бы рассмотреть для этого варианта использования, поскольку может быть проще поддерживать логику обновления в одном месте вместо отслеживания нескольких событий или блоков ловушек.

// Updated to use a common structure for reducer
// Also kept the reducer pure so it can be moved out of the component

const selectsReducer = (state, action) => {
  const { type, payload } = action;
  switch (type) {
    case "update_planet": {
      const newCountry = payload.countries.find(
        (c) => c.planet === payload.value
      ).id;
      return {
        planet: payload.value,
        country: newCountry,
        city: payload.cities.find((c) => c.country === newCountry).id,
      };
    }
    case "update_country": {
      return {
        ...state,
        country: payload.value,
        city: payload.cities.find((c) => c.country === payload.value).id,
      };
    }
    case "update_city": {
      return {
        ...state,
        city: payload.value,
      };
    }
    default:
      return { ...state };
  }
};
// Inside the component
const [selects, dispatchSelects] = useReducer(selectsReducer, record);

Оригинал

Хотя уродство или нет скорее основано на мнении, возможно, потенциально организованным решением для обработки состояний, зависящих друг от друга, является использование useReducer.

Живая демонстрация приведенного ниже примера: stackblitz

Вот базовый пример, который обновляет зависимые значения, например, если country обновляется, то city также изменится на первое доступное city в новом country, чтобы соответствовать ему.

Это поддерживает актуальность значения в списках выбора и гарантирует, что onSave всегда будет получать обновленные значения из состояния.

const selectsReducer = (state, action) => {
  const { type, planet, country, city } = action;
  let newPlanet,
    newCountry,
    newCity = "";
  switch (type) {
    // 👇 Here to update all select values (the next 2 cases also run)
    case "update_planet": {
      newPlanet = planet;
    }
    // 👇 Here to update country and city (the next case also run)
    case "update_country": {
      newCountry = newPlanet
        ? countries.find((c) => c.planet === newPlanet).id
        : country;
    }
    // 👇 Here to update only city
    case "update_city": {
      newCity = newCountry
        ? cities.find((c) => c.country === newCountry).id
        : city;
      return {
        planet: newPlanet || state.planet,
        country: newCountry || state.country,
        city: newCity || state.city,
      };
    }
    default:
      return record;
  }
};

const [selects, dispatchSelects] = useReducer(selectsReducer, record);

Поскольку отношение зависимости имеет только одно направление, простым подходом было бы просто очистить все, что зависит от того, которое изменилось, когда оно изменилось. Так:

  • очистить страну и город при выборе планеты
  • очистить город при выборе страны
    <select value = {selectedPlanet.id} onChange = {(e) => setPlanetAndClearCountryAndCity(e)}>
      {planets.map(p => (
        <option key = {p.id} value = {p.id} name = {p.name} />
      )}
    </select>

    <select value = {selectedCountry.id} onChange = {(e) => setCountryAndClearCity(e)}>
      {filteredCountries.map(c => (
        <option key = {c.id} value = {c.id} name = {c.name} />
      )}
    </select>

И поместите некоторую проверку, которая требует всех 3, прежде чем кнопка сохранения будет включена.

Также вы можете подумать о том, чтобы не очищать страну или город, если у них все еще есть действительные варианты. Но это похоже на больше работы, чем оно того стоит.

Я думаю, что некоторые другие ответы слишком усложняют это! Ваше предложение очистить состояние через обработчик onChange является хорошим.

Linda Paiste 22.12.2022 19:21

Вы можете использовать useEffect хук для обработки обновлений поля выбора. Таким образом, вы не пропустите ни одного обновления значений.

демо


export const SelectList = ({ record, onSave, planets, countries, cities }) => {
  const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
  const [selectedCountry, setSelectedCountry] = useState(record.country);
  const [selectedCity, setSelectedCity] = useState(record.city);

  /** handle filteredValues in state */
  const [filteredCountries, setFilteredCountries] = useState([]);
  const [filteredCities, setFilteredCities] = useState([]);

  /** use useEffect to update values */
  useEffect(() => {
    const filteredCountries = countries.filter((c) => c.planet === selectedPlanet);
    setFilteredCountries(filteredCountries);
    setSelectedCountry(filteredCountries[0].id);
  }, [selectedPlanet]);

  useEffect(() => {
    const filteredCities = cities.filter((c) => c.country === selectedCountry);
    setFilteredCities(filteredCities);
    setSelectedCity(filteredCities[0].id);
  }, [selectedCountry]);

  return (
    <div>
      <select value = {selectedPlanet}
        onChange = {(e) => setSelectedPlanet(e.target.value)}>
        {planets.map((p) => (
          <option key = {p.id} value = {p.id}>{p.name}</option>
        ))}
      </select>
      <select value = {selectedCountry}
        onChange = {(e) => setSelectedCountry(e.target.value)}>
        {filteredCountries.map((c) => (
          <option key = {c.id} value = {c.id}>{c.name}</option>
        ))}
      </select>
      <select
        value = {selectedCity}
        onChange = {(e) => setSelectedCity(e.target.value)}>
        {filteredCities.map((c) => (
          <option key = {c.id} value = {c.id}>{c.name}</option>
        ))}
      </select>
      <button onClick = {() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity })}>Save</button>
    </div>
  );
};


Я думаю, что использование хука React.useMemo для двух переменных: filteredCountries и filteredCities будет лучшим решением для вас. Это не сделает ваш код уродливым, и вам не придется фильтровать дважды. Вот обновленный код.

import * as React from 'react';

// record, onSave, planets, countries, cities passed as props

const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
const [selectedCountry, setSelectedCountry] = useState(record.country);
const [selectedCity, setSelectedCity] = useState(record.city);

const filteredCountries = React.useMemo(() => {
  const _countries = countries.filter(c => c.planet === selectedPlanet.id);
  setSelectedCountry(_countries[0]);
  return _countries;
}, [selectedPlanet]);

const filteredCities = React.useMemo(()=> {
  const _cities = cities.filter(c => c.country === selectedCountry.id);
  setSelectedCity(_cities[0]);
  return _cities;
}, [setSelectedCountry])

return (
  <div>
    <select value = {selectedPlanet.id} onChange = {(e) => setSelectedPlanet(e.target.value)}>
      {planets.map(p => (
        <option key = {p.id} value = {p.id} name = {p.name} />
      )}
    </select>

    <select value = {selectedCountry.id} onChange = {(e) => setSelectedCountry(e.target.value)}>
      {filteredCountries.map(c => (
        <option key = {c.id} value = {c.id} name = {c.name} />
      )}
    </select>


    <select value = {selectedCity.id} onChange = {(e) => setSelectedCity(e.target.value)}>
      {filteredCities.map(c => (
        <option key = {c.id} value = {c.id} name = {c.name} />
      )}
    </select>

    <button onClick = {() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity ) }
  </div>
);

Один из способов — справиться с этим с помощью хука useEffect. это решение представляет собой простую логику, и, очевидно, вы можете справиться с ним с более подробной информацией и условиями.

import React, { useEffect, useState } from "react";

const planets = [
  { id: 1, name: "earth", value: "earth" },
  { id: 2, name: "mars", value: "mars" },
  { id: 3, name: "jupiter", value: "jupiter" }
];
const countries = [
  { id: 1, name: "USA", value: "USA", planet: "earth" },
  { id: 2, name: "UAE", value: "UAE", planet: "mars" },
  { id: 3, name: "Canada", value: "Canada", planet: "mars" },
  { id: 4, name: "England", value: "England", planet: "earth" }
];
const cities = [
  { id: 1, name: "Los Angeles", value: "Los Angeles", country: "USA" },
  { id: 2, name: "Dubai", value: "Dubai", country: "UAE" },
  { id: 3, name: "Torento", value: "Torento", country: "Canada" },
  { id: 4, name: "Washington", value: "Washington", country: "USA" },
  { id: 5, name: "London", value: "London", country: "England" }
];

export default function MaterialUIPickers() {
  const [selectedPlanet, setSelectedPlanet] = useState(planets[0].name);
  const [selectedCountry, setSelectedCountry] = useState("");
  const [selectedCity, setSelectedCity] = useState("");

  const [countryItems, setCountryItems] = useState([]);
  const [cityItems, setCityItems] = useState([]);

  useEffect(() => {
    setCountryItems([]); // reset select
    setCityItems([]); // reset select
    setSelectedCountry(""); // reset select
    setSelectedCity(""); // reset select
    selectedPlanet &&
      setCountryItems(countries.filter((c) => c.planet === selectedPlanet));
  }, [selectedPlanet]);

  useEffect(() => {
    setCityItems([]); // reset select
    setSelectedCity(""); // reset select

    selectedCountry &&
      setCityItems(cities.filter((c) => c.country === selectedCountry));
  }, [selectedCountry]);

  return (
    <div style = {{ display: "flex-box", width: "vh" }}>
      <select
        value = {selectedPlanet.id}
        onChange = {(e) => setSelectedPlanet(e.target.value)}
      >
        {planets.map((p) => (
          <option key = {p.id} value = {p.name} name = {p.name}>
            {p.name}
          </option>
        ))}
      </select>

      <select
        value = {selectedCountry?.id}
        onChange = {(e) => setSelectedCountry(e.target.value)}
      >
        {countryItems.map((c) => (
          <option key = {c.id} value = {c.name} name = {c.name}>
            {c.name}
          </option>
        ))}
      </select>

      <select
        value = {selectedCity?.id}
        onChange = {(e) => setSelectedCity(e.target.value)}
      >
        {cityItems.map((c) => (
          <option key = {c.id} value = {c.name} name = {c.name}>
            {c.name}
          </option>
        ))}
      </select>

      <button
        onClick = {() =>
          console.info({
            planet: selectedPlanet,
            country: selectedCountry,
            city: selectedCity
          })
        }
      >
        log selcets
      </button>
    </div>
  );
}

https://codesandbox.io/s/kind-wilson-zpenc?file=/src/App.js это ссылка на песочницу

Другие вопросы по теме