Почему вызов одной функции » setState` обновляет совершенно отдельное значение состояния?

#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
 

Преимущества этого подхода заключаются в том, что

  1. Вам не нужно делать двойной рендеринг. Исходная версия устанавливает сортировку, визуализацию, затем устанавливает отображаемые данные и снова визуализирует
  2. Невозможно иметь несовпадающие состояния. Например, в промежутке между первым и вторым рендерингом значение 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>
  )
}