Передача компонентов с состоянием из массива - состояние внука и родительское состояние не выравниваются - PullRequest
0 голосов
/ 12 марта 2020

Я пишу редактируемый пользователем компонент, который поддерживает пользовательские изменения в своем состоянии. Я хочу иметь возможность использовать компонент двумя способами: 1: Жестко запрограммирован автором по одному, или 2: , сгенерированный из массива из состояния родительского компонента. У меня проблемы с синхронизацией состояний во втором сценарии. Я хочу, чтобы компонент был съемным, поэтому у него есть кнопка «удалить меня», которая должна связываться с состоянием родителя с помощью функции обратного вызова prop.

Сценарий:

Позвольте сказать У меня есть родительский компонент, который имеет массив в своем состоянии. Из этого массива дочерние компоненты отображаются с помощью оператора .map:

// in ParentComponent.js:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 2"},
    {coords: Array(2), popupContent: "Popup 3"},
    ... etc ...
  ]
}

// In the return:

this.state.markers.map( (marker, index) => (
  <Marker key={index}>
    <Popup sourceKey={index} 
      setContentCallback={this.saveContentToState} 
      removalCallback={this.removalCallback} >
      {marker.popupContent}
    </Popup>
  </Marker>
))

Компонентом, о котором идет речь, является granchild, <Popup/>. Из Popup. js пользователь может вносить изменения в содержимое компонента, и эти изменения сохраняются в состоянии Popup:

// In Popup.js

   state = { 
     content: this.props.children,
     inputValue: this.props.children
   }

   onEditHandler = { 
     this.setState({inputValue: e.target.value}) 
   }

   saveEdits = () => {
      if (this.props.saveContentCallback){
         this.props.saveContentCallback(this.state.inputValue, this.props.sourceKey)
      }
      this.setState({
         content: this.state.inputValue,
      })
   }

   removeSource = () => {
      if(this.props.removalCallback){
         this.props.removalCallback(this.props.sourceKey)
      } else {
         // internal leaflet function to remove a popup from a map
         this.thePopup.leafletElement._source.remove()
      }
   }

// Within the return function:
   return ( 
     <>
       <ContentEditableDiv onChange={this.onEditHandler}>
         { this.state.content }
       </ContentEditableDiv>
       <div onClick={this.saveEdits}>Save</div>
       <div onClick={this.removeSource}>Remove me</div>
     </>
   )

Вы можете увидеть, как компонент будет поддерживать изменения в своем собственном состоянии. из функции saveEdits в любом из сценариев ios, упомянутых выше. Но для того, чтобы сообщать об изменениях с состоянием родителя, он использует реквизиты removalCallback и saveContentCallback. Итак, вернемся к ParentComponent.js,

  removalCallback = index => {
    mapRef.current.leafletElement.closePopup()
    this.setState(prevState => {
       prevState.markers.splice(index, 1)
       return {
          markers: prevState.markers
       }
    })
  }

   saveContentToState = (content, index) => {
      this.setState( prevState => {
         const newMarkers = prevState.markers
         newMarkers[index].popupContent = content
         return {
            ...this.state.newMarkers
         }
      })
   }

Ожидаемое поведение

Когда во всплывающем окне нажата кнопка «Удалить», я ожидаю, что будет вызван обратный вызов. Когда вызывается обратный вызов, он должен удалить это всплывающее окно из «маркеров» массива состояний ParentComponent, а ParentComponent должен выполнить повторную визуализацию только с оставшимися маркерами с их popupContent. Так, например, если я начну с этого массива:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 2"},
    {coords: Array(2), popupContent: "Popup 3"},
  ]
}

, а затем нажмите кнопку remove me во всплывающем окне 2, я должен получить этот массив:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 3"},
  ]
}

С два маркера с всплывающими окнами с надписью «Всплывающее окно 1» и «Всплывающее окно 3».

Фактическое поведение - ошибка

Итак, я действительно получаю массив ожидаемых в состоянии ParentComponent, так как я просто написал выше. Тем не менее, внутренние состояния всплывающих окон не сотрудничают. Когда я нажимаю кнопку remove me в всплывающем окне № 2, я получаю 2 всплывающих окна, но их содержимое - «Всплывающее окно 1» и «Всплывающее окно 2». Когда я смотрю на внутреннее состояние каждого <Popup /> компонента, content для каждого - это «Popup 1» и «Popup 2» соответственно. Как будто при удалении i th всплывающего окна его внутреннее состояние каким-то образом передается в i+1 th всплывающего окна, которое передается через все всплывающие окна в массиве.

Рабочая демонстрация Задача

Это проект листовки с реакцией, но я чувствую, что это вопрос управления состоянием реагирования. Откройте визуализацию кодов и окно, и вы увидите 5 всплывающих окон. Когда вы нажмете «удалить меня» в любом всплывающем окне (кроме последнего), вы увидите сдвиг всех номеров всплывающего окна. На вкладке компонентов реагирующих инструментов вы увидите, что массив состояний <Map /> (т.е. <ParentComponent>) обновляется правильно. Но, глядя на каждое внутреннее состояние <EditablePopup />, они не соответствуют состоянию родительского (<Map>) компонента. Я знаю, что что-то вроде state = { content: this.props.something } может вызвать проблемы, но я не уверен, что это виновник в этом сценарии.

Что здесь не так? Разве все эти <Marker/> и <Popup/> компоненты не должны перегенерироваться каждый раз при срабатывании removalCallback или saveContentCallback, так как это обновляет состояние родителя и должно вызывать рендеринг родителя? Я попытался добавить this.forceUpdate в эти обратные вызовы в родительском компоненте, но это ничего не сделало. Извините за длинный вопрос, спасибо за чтение .

Ответы [ 2 ]

3 голосов
/ 18 марта 2020

Причина в том, что вы не передаете уникальный ключ для каждого дочернего элемента в списке. Без уникального ключа React не смог бы различить, был ли удален элемент или просто изменено содержимое.

Итак, когда вы удаляете элемент, алгоритм сравнения React считает, что изменилось только содержимое, потому что ключ не изменился

Самый простой способ проверить это изменить ключ для popupContent как.

<Marker key={marker.popupContent} position={marker.coords}>
  ...
</Marker>

Но это не решение. Создайте уникальный ключ для каждого элемента, чтобы избежать проблем в будущем.

Подробнее о том, как работает алгоритм сравнения React

Как алгоритм Diff реализован в Reactjs?

Убедитесь, что ваши ключи уникальны!

1 голос
/ 18 марта 2020

вам нужно изменить line136 из editablePopup.js, чтобы получить самых последних детей.

{Parser(this.props.children)}

рабочий пример

https://codesandbox.io/s/removal-callback-question-z9q6i

Я также заметил, что вы изменяете старое состояние, а не новое.

       const newMarkers = prevState.markers // But here you're mutating the old state. you should mutate copy of the state.
       newMarkers[index].popupContent = content
       return {
          ...this.state.newMarkers  
       }

То же, что и ниже, вы мутируете в предыдущем штате.

    this.setState(prevState => {
       prevState.markers.splice(index, 1) // User array filter with index !== i. So you will get a copy with filtered array. 
       return {
          markers: prevState.markers
       }
    })

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...