#javascript #reactjs
Вопрос:
😄👋
У меня возникла проблема, когда одна setState
функция обновляет два отдельных значения состояния.
Я пытаюсь создать небольшой сортируемый массив с реакцией. Программа должна вести себя так:
- извлекает данные при монтировании
- хранит эти данные в обоих
unsortedData
amp;displayedData
государственных крючках - когда пользователь нажимает кнопку «переключить сортировку», массив
displayedData
сортируется (с помощью.sort()
) - при втором нажатии кнопки «переключить сортировку» должно быть установлено
displayedData
то же значение, что и приunsortedData
восстановлении исходного порядка
Однако при первом щелчке переключателя сортировки он сортирует оба unsortedData
amp; displayedData
, что означает, что я теряю исходный порядок данных. Я понимаю, что мог бы сохранить их порядок, но я хочу знать, почему эти значения состояния кажутся связанными.
Рабочий код Stackblitz здесь.
GIF показывает проблему здесь
Я ни за что на свете не могу понять, где это происходит. Я не вижу никаких раздражающих ссылок на объекты/массивы (я распространяюсь на новые объекты/массивы).
Код здесь:
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [displayedData, setDisplayedData] = useState([])
const [isSorted, setisSorted] = useState(false)
const handleSorting = () => setisSorted(!isSorted)
useEffect(() => {
if (isSorted === true) setDisplayedData([...unsortedData.sort()]) // sort data
if (isSorted === false) setDisplayedData([...unsortedData]) // restore original data order
}, [isSorted, unsortedData])
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData]) // store original data order in "unsortedData"
setDisplayedData([...mockData])
}, [])
return (
<div>
{displayedData.map(item => item)}
<br />
<button onClick={() => handleSorting()}>Toggle sorting</button>
</div>
)
}
Кроме того, зачем мне нужен эффект использования? Почему я не могу просто переместить содержимое useEffect (в котором есть операторы if) в handleSorting
функцию?
Комментарии:
1. Подсказка: ваш щелчок срабатывает
handleSorting
, что изменяет значение одного состояния, что запускает повторный вызов, который запускает вашиuseEffect
вызовы, которые, в свою очередь, делают что-то, что отвечает на ваш вопрос.2. Привет @Mike’Pomax’Camermans! Спасибо за помощь! Я понимаю ход событий, но в последнем случае, эффект использования, я не запускаю
setUnsortedData
, да, его значение обновляется… понятия не имею, почему!3. Нет, но вы работаете
unsortedData.sort()
, поэтому вы изменяете этот массив (только не через связанную с ним функцию обновления состояния).
Ответ №1:
.sort
Функция изменяет массив, поэтому, когда вы делаете это:
setDisplayedData([...unsortedData.sort()])
Вы мутируете несортированные данные, а затем делаете их копию. Поскольку вы изменили исходный массив, это изменение может отображаться на экране при повторной отправке компонента.
Поэтому минимальным решением было бы сначала скопировать, а затем отсортировать:
setDisplayedData([...unsortedData].sort())
Кроме того, зачем мне нужен эффект использования? Почему я не могу просто переместить содержимое useEffect (в котором есть операторы if) в функцию handleSorting?
Перемещение его в обработку должно быть возможно, но на самом деле я хотел бы предложить другой вариант: иметь только два состояния, несортированные данные и логическое значение для сортировки. Затем отображаемые данные представляют собой вычисленное значение, основанное на этих двух.
const [unsortedData, setUnsortedData] = useState([3, 9, 6]) // initialize with mock data
const [isSorted, setisSorted] = useState(false)
const displayedData = useMemo(() => {
if (isSorted) {
return [...unsortedData].sort();
} else {
return unsortedData
}
}, [unsortedData, isSorted]);
const handleSorting = () => setisSorted(!isSorted)
// No use useEffect to sort the data
// Also no useEffect for the mock data, since i did that when initializing the state
Преимущества этого подхода заключаются в том, что
- Вам не нужно делать двойной рендеринг. Исходная версия устанавливает сортировку, визуализацию, затем устанавливает отображаемые данные и снова визуализирует
- Невозможно иметь несовпадающие состояния. Например, в промежутке между первым и вторым рендерингом значение isSorted равно true, и все же отображаемые данные на самом деле еще не отсортированы. Но даже без двойного рендеринга наличие нескольких состояний требует, чтобы вы были бдительны и каждый раз, когда вы обновляете несортированные или пересортированные данные, вы также не забывали обновлять отображаемые данные. Это просто происходит автоматически, если это вычисленное значение, а не независимое состояние.
Комментарии:
1. Это так полезно! Большое спасибо! Так что это была какая-то досадная мутация ссылок на объекты! 😩 Ах да, мне нравится такой подход! Меньше изменений состояния = меньше повторной визуализации, очень хороший момент! Спасибо, приятель, очень тебе благодарен! 😄
2. Попытка сделать все это в одной функции, похоже, не работает: « const handleSorting = () => { setisSorted(!isSorted), если (isSorted === true) setDisplayedData([…несортированные данные].сортировка()) // сортировка данных, если (isSorted === false) setDisplayedData([…несортированные данные]) // восстановить исходный порядок данных} ` » первый щелчок переключателя ничего не делает, следующий щелчок работает. Визуализированный пользовательский интерфейс, похоже, является одним из рендеров, стоящих за тем, что я хочу… когда isSorted имеет значение false, он сортируется, и наоборот. пример стекблитца
3. Это потому, что я должен делать это эффективным способом («прослушивать» изменения состояния в массиве dep и запускать эти функции на основе этих изменений)? Было бы гораздо более лаконично, если бы я мог сделать это в одной функции.
4.
Trying to do it all in one function doesn't seem to work
Причина, по которой код всегда отстает на один шаг, заключается в том, что у вас есть инструкции if в обратном порядке.isSorted
содержит старое значение, а не новое значение. Итак, еслиisSorted
это правда, то вы хотите отменить сортировку массива, но вместо этого вы его сортируете.5. Спасибо за помощь @nicholas-tower! Я действительно ценю это! Ах да… но я обновляю значение состояния перед операторами if. Почему изменение состояния не влияет на компонент перед операторами if? Если вы рекомендовали прочитать об этом типе рендеринга реакции, я бы с удовольствием его послушал! 😄
Ответ №2:
Если вы хотите отображать отсортированный массив пользователю только тогда, когда пользователь нажимает кнопку, вы можете использовать этот оператор кода (изначально взятый из списка задач vue3).
const filters = {
none: (data) => [...data],
sorted: (data) => [...data].sort((a, b) => a - b),
}
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [currentFilter, setCurrentFilter] = useState('none')
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData])
}, [])
return (
<div>
<ul>
{/* All magic is going here! */}
{filters[currentFilter](unsortedData).map(item => <li key={item}>{item}</li>)}
</ul>
<br />
<button
onClick={() =>
setCurrentFilter(currentFilter === 'none' ? 'sorted' : 'none')}>
Toggle sorting
</button>
</div>
)
}