从数组中渲染有状态组件 - 孙子状态和 parent 状态不对齐

Rending stateful components from array - grandchild state and parent state not aligning

我正在编写一个 user-editable 组件,它可以在其状态下维护用户的更改。我希望能够以两种方式使用该组件:1: 由作者一次硬编码,或者 2: 从一个来自 parent 组件状态的数组。在第二种情况下,我无法同步状态。我希望组件是可移动的,所以它有一个 "remove me" 按钮,它应该通过使用回调函数 prop.

与 parent 的状态通信。

场景:

假设我有一个 parent 组件,其状态为数组。从这个数组中,child 个组件使用 .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 函数中看到组件如何在其自身状态内保持变化。但是为了与 parent 的状态进行通信,它使用了属性 removalCallbacksaveContentCallback。所以,回到 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
         }
      })
   }

预期行为

当在 Popup 上单击 'remove' 按钮时,我希望回调被调用。当调用回调时,它应该从 ParentComponent 的状态数组 'markers' 中删除该弹出窗口,并且 ParentComponent 应该仅使用剩余的标记及其关联的 popupContent 重新呈现。例如,如果我从这个数组开始:

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

然后点击弹出窗口 2 上的 remove me 按钮,我应该得到这个数组:

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

有两个带有弹出窗口的标记,上面写着 "Popup 1" 和 "Popup 3"。

实际行为 - 错误

所以我确实在 ParentComponent 状态中得到了期望数组,正如我在上面写的那样。但是,弹出窗口的内部状态不合作。当我单击弹出窗口 #2 上的 remove me 按钮时,我得到 2 个结果弹出窗口,但它们的内容是 "Popup 1" 和 "Popup 2"。当我查看每个 <Popup /> 组件的内部状态时,每个组件的 content 分别为 "Popup 1" 和 "Popup 2"。就好像当第 i 个弹出窗口被删除时,它的内部状态以某种方式转移到第 i+1 个弹出窗口,它通过数组中的所有 Popup 转移。

Working Demo of the Problem

这是一个react-leaflet项目,但我觉得这是一个反应状态管理问题。打开 codesandbox 的渲染图,你会看到 5 个弹窗。当您在任何弹出窗口(最后一个除外)上单击 'remove me' 时,您会看到所有弹出窗口的数字都发生变化。在 React 开发工具组件选项卡中,您会看到 <Map />(即 <ParentComponent>)状态数组正在正确更新。但是查看每个 <EditablePopup /> 内部状态,这些与来自 parent (<Map>) 组件的状态不对应。我知道像 state = { content: this.props.something } 这样的东西会导致问题,但我不确定这是否是这种情况下的罪魁祸首。

这里出了什么问题? all 这些 <Marker/><Popup/> 组件不应该在每次 removalCallbacksaveContentCallback 触发时重新呈现,因为它会更新 parent 的状态并且应该触发 parent 的重新渲染? 我尝试 在 parent 组件内的这些回调中添加 this.forceUpdate,但没有任何作用。抱歉问题太长了,感谢阅读

原因是您没有为列表中的每个子元素传递唯一键。如果没有唯一键,React 无法区分元素是被删除了还是只是内容被更改了。

所以,当你删除项目时,React 的 diff 算法认为只有内容发生了变化,因为密钥没有改变

最简单的测试方法是更改​​ popupContent 之类的密钥。

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

但这不是解决方案。为每个元素创建一个唯一的键,以防止将来出现问题。

有关 React 差异算法工作原理的更多信息

How Diff Algorithm is implemented in Reactjs?

确保您的密钥是唯一的!

您需要更改 editablePopup.jsline136 以获得最新的 children。

{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  
       }

与下面的拼接相同,您正在改变 prevState。

    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
       }
    })