从数组中渲染有状态组件 - 孙子状态和 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 的状态进行通信,它使用了属性 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
}
})
}
预期行为
当在 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/>
组件不应该在每次 removalCallback
或 saveContentCallback
触发时重新呈现,因为它会更新 parent 的状态并且应该触发 parent 的重新渲染? 我尝试 在 parent 组件内的这些回调中添加 this.forceUpdate
,但没有任何作用。抱歉问题太长了,感谢阅读。
原因是您没有为列表中的每个子元素传递唯一键。如果没有唯一键,React 无法区分元素是被删除了还是只是内容被更改了。
所以,当你删除项目时,React 的 diff 算法认为只有内容发生了变化,因为密钥没有改变
最简单的测试方法是更改 popupContent
之类的密钥。
<Marker key={marker.popupContent} position={marker.coords}>
...
</Marker>
但这不是解决方案。为每个元素创建一个唯一的键,以防止将来出现问题。
有关 React 差异算法工作原理的更多信息
确保您的密钥是唯一的!
您需要更改 editablePopup.js
的 line136
以获得最新的 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
}
})
我正在编写一个 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 的状态进行通信,它使用了属性 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
}
})
}
预期行为
当在 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/>
组件不应该在每次 removalCallback
或 saveContentCallback
触发时重新呈现,因为它会更新 parent 的状态并且应该触发 parent 的重新渲染? 我尝试 在 parent 组件内的这些回调中添加 this.forceUpdate
,但没有任何作用。抱歉问题太长了,感谢阅读。
原因是您没有为列表中的每个子元素传递唯一键。如果没有唯一键,React 无法区分元素是被删除了还是只是内容被更改了。
所以,当你删除项目时,React 的 diff 算法认为只有内容发生了变化,因为密钥没有改变
最简单的测试方法是更改 popupContent
之类的密钥。
<Marker key={marker.popupContent} position={marker.coords}>
...
</Marker>
但这不是解决方案。为每个元素创建一个唯一的键,以防止将来出现问题。
有关 React 差异算法工作原理的更多信息
确保您的密钥是唯一的!
您需要更改 editablePopup.js
的 line136
以获得最新的 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
}
})