Как правильно стереть элемент из массива состояний React?

Создание компонентов на основе массива состояний и присвоение каждому компоненту атрибута setState массива приводит к странному поведению. Может из-за параллельных изменений состояния? Попробуйте приведенный ниже фрагмент. Щелчок по квадрату должен удалить его. Вместо этого всегда будет удаляться последний созданный квадрат.

Проблема, кажется, заключается в useEffect() в компоненте Square. Хотя во фрагменте это действительно бесполезно, я использую что-то подобное, чтобы сделать элементы перетаскиваемыми. Может ли кто-нибудь объяснить это поведение и предложить решение? Я чувствую, что пропустил что-то важное о React здесь.

function Square(props){
  const [pos, setPos] = React.useState([props.top, props.left])

  React.useEffect(() => {
    props.setSquares(prevState => prevState.map((square, index) => index === props.index ? {...square, top: pos + 20} : square
    ))
  }, [pos])

  function deleteMe(){
    props.setSquares(prevState => prevState.filter((square, index) => index !== props.index))
  }

  return <div onClick={deleteMe} 
    style={{top: pos[0]+'px', left: pos[1]+'px'}}             
    className='square'>
  </div>
}

function App(){
  const [squares, setSquares] = React.useState([
    {top: 20, left: 0},{top: 20, left: 100},
    {top: 20, left: 200},{top: 20, left: 300}])

  React.useEffect(() => console.log('rerender', ...squares.map(s => s.left)))

  return <div>{squares.map((square, index) => <Square 
    setSquares={setSquares} index={index} 
    top={square.top} left={square.left}/>)}</div>
}
ReactDOM.render(<App/>, document.getElementById('root'))
.square{
  position: absolute;
  width: 80px;
  height: 80px;
  background: orange;
}
<div id='root'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

Редактировать: Как предложил Ник в комментариях, я изменил методы setSquare, чтобы использовать prevState.map и filter, чтобы не изменять предыдущее состояние непреднамеренно.

Однако проблема остается ... всегда последний квадрат исчезает на экране. Использование эффекта при каждом повторном рендеринге показывает, что состояние обновляется правильно. Что мне здесь не хватает?


person Michael    schedule 06.02.2020    source источник


Ответы (1)


Вы должны помнить, что оператор распространения является поверхностной копией, что означает, что newSquares[props.index].top = pos[0] + 20 в следующем коде изменяет объект в вашем массиве текущего состояния.

props.setSquares(prevState => {
  const newSquares = [...prevState]
  newSquares[props.index].top = pos[0] + 20 // this is mutating current state
  return newSquares
})

Один из вариантов исправить это - сделать это вместо этого:

props.setSquares(prevState => {
  const newSquares = [...prevState]
  newSquares[props.index] = { 
    ...newSquares[props.index],
    top: pos[0] + 20
  };
  return newSquares
})

Другой вариант — просто отобразить существующее состояние и просто вернуть что-то новое для элемента, который вы хотите изменить:

props.setSquares(prevState => {
  return prevState.map((el, i) => {
    return i === props.index ? { ...el, top: pos[0] + 20 } : el;
  });
})
person Nick    schedule 07.02.2020
comment
Спасибо за предложения. Я предположил, что неглубокое копирование не будет вредным, потому что оно происходит внутри метода setSquares, где prevState — это только локальная переменная, верно? Игра с prevState здесь не должна влиять на текущее состояние. Также правильная копия (например, созданная map) приводит к такому же поведению. В то время как console.log(squares) прямо перед возвратом Apps показывает, что правильный квадрат был удален из состояния, на экране исчез неправильный квадрат. - person Michael; 07.02.2020
comment
Я не верю, что prevState является глубокой копией объекта состояния даже в функции обратного вызова - совершенно уверен, что это было бы супер неэффективно. - person Nick; 07.02.2020
comment
Я думаю, что в этом примере вы смешиваете переназначение и мутацию. Более подходящим примером было бы, если бы вы указали, что это объект, и вы изменяли свойство этого объекта. - person Nick; 07.02.2020
comment
Вы абсолютно правы в этом, конечно. Я немного изменил codepen. Я все еще убежден, что нельзя связываться с prevState внутри setState. - person Michael; 07.02.2020
comment
Проблема с тем, что prevState каким-то образом не зависит от текущего состояния, означает, что React должен был бы делать глубокую копию под капотом, что было бы очень неэффективно, поскольку каждый компонент, который полагается на часть вашего состояния, тогда перерисовать - person Nick; 07.02.2020
comment
вы были абсолютно правы насчет роли prevState. Изменение предыдущего состояния и возврат неглубокой копии действительно меняет состояние, просто повторной визуализации не происходит. Вот почему кажется, что ничего не меняется. Однако странное поведение все еще остается. - person Michael; 07.02.2020