Я пытаюсь сделать 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 не вызываются при смене планеты. Я знаю, что могу просто вызвать их в этом случае, но это сделало бы код намного уродливее, потому что мне пришлось бы дублировать фильтрацию по стране и городу. Есть ли лучший способ обойти это?
использование хука useMemo решит вашу проблему. вы можете найти код в моем сообщении





Обновлять
Я обновил пример кода для подхода 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 является хорошим.
Вы можете использовать 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 это ссылка на песочницу
почему страны и города не фильтруются снова каждый раз при рендере?