'setState' хука useState происходит дважды за один вызов

Есть доска с квадратами, их значение зависит от массива, это обрабатывается с помощью хука useState. Каждый щелчок должен повышать значение на единицу, но, к сожалению, он увеличивает его на два (кроме первого щелчка).

Мои вопросы:

(1) Почему это происходит, (2) как этого избежать и, в целом, (3) есть ли лучший способ обработать такой массив с помощью крючков.

let emptyBoard = Array.from({ length: parseInt(props.rows, 10) }, () =>
    new Array(parseInt(props.columns, 10)).fill(0)
  );

  const [squaresValues, setSquaresValue] = useState(emptyBoard);

  function onClick(id) {
    const [rowIndex, cellIndex] = id;
    console.log("the " + id + " square was clicked");
    setSquaresValue(prevValues => {      
      let newBoard = [...prevValues];
      console.log("before: " + newBoard[rowIndex][cellIndex]);
      newBoard[rowIndex][cellIndex] = newBoard[rowIndex][cellIndex] + 1;
      console.log("after: " + newBoard[rowIndex][cellIndex]);
      return newBoard;
    }
    );
  }

Журнал:

the 3,0 square was clicked 
before: 0 
after: 1 
the 3,0 square was clicked 
before: 1 
after: 2 
before: 2 
after: 3 

Как видно, при втором щелчке значение увеличивается дважды при каждом щелчке.


person yoni    schedule 07.07.2020    source источник
comment
github.com/facebook/react/issues/12856 В процессе разработки дважды вызывается setState - это принудительная функция с помощью реакции, чтобы убедиться, что ваш setState независим и идемпотентен   -  person Udaya Prakash    schedule 07.07.2020


Ответы (2)


Вы все еще изменяли состояние, если у вас есть чистые компоненты, они не будут повторно отображаться при изменении. Выполнение полной копии состояния с помощью JSON.parse - плохая идея, если у вас есть чистые компоненты, потому что все будет повторно отрисовано.

let newBoard = [...prevValues];
newBoard[rowIndex] = [...newBoard[rowIndex]];
newBoard[rowIndex][cellIndex] =
newBoard[rowIndex][cellIndex] + 1;
person HMR    schedule 07.07.2020
comment
Ваше решение работает, спасибо, но я не понимаю, почему ... не могли бы вы объяснить? - person yoni; 07.07.2020
comment
Я согласен, это лучшее решение - person tanmay; 07.07.2020
comment
@yoni Вы изменили состояние, и это может нарушить повторный рендеринг. Дополнительную информацию о том, как не изменять состояние, можно найти здесь - person HMR; 07.07.2020

Как упомянул Удая Пракаш в комментарии выше, он вызывается дважды, чтобы убедиться, что ваш setState независим и идемпотентен. Итак, если я правильно понимаю, то, что он был вызван дважды, не является ошибкой, но ваши значения, измененные во второй раз, - это ошибка.

Вот комментарий Дэна Абрамова из того же выпуска GitHub:

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

Мы можем исправить это, глубоко скопировав ваш prevValues вместо поверхностного копирования с помощью оператора распространения. Как вы, возможно, уже знаете, существует несколько способов глубокого копирования вашего объекта, на данный момент мы могли бы использовать JSON.parse(JSON.stringify(...), который вы можете заменить на отсюда ваш любимый

setSquaresValue(prevValues => {
  let newBoard = JSON.parse(JSON.stringify(prevValues)); // <<< this
  console.log("before: " + newBoard[rowIndex][cellIndex]);
  newBoard[rowIndex][cellIndex] = newBoard[rowIndex][cellIndex] + 1;
  console.log("after: " + newBoard[rowIndex][cellIndex]);
  return newBoard;
});

Я как бы смоделировал это в codeandbox здесь, если вы хотите поиграть.

person tanmay    schedule 07.07.2020
comment
Просто поясню, неглубокая копия здесь не работает, потому что это двумерный массив, и только одно измерение может быть неглубоко скопировано. Я прав или еще что-то путаю? Кстати, отличный ответ и объяснение - person Udaya Prakash; 07.07.2020
comment
@UdayaPrakash Думаю, ты прав. Все, что находится на втором уровне или выше, сохранит ссылку (при неглубоком копировании), и, следовательно, возникает проблема. - person tanmay; 07.07.2020
comment
tanmay, @UdayaPrakash, так что вы предлагаете, чтобы обойти эту проблему? - person yoni; 07.07.2020
comment
@yoni Если вы уже используете lodash, вы можете использовать cloneDeep для глубокого клонирования, JSON.parse(JSON.stringify(...)) также подойдет, если у вас есть небольшой объект, я считаю, или вы можете пойти с подходом в другом ответе HMR - person tanmay; 07.07.2020