React渲染可编辑组件数组时删除功能异常问题求助
我实现了一个支持contentEditable的组件EditableComponent,代码如下:
EditableComponent.js
const EditableComponent = (props) => { return <p contentEditable>{props.children}</p>; };
在ParentComponent中,我用useState维护一个组件数组someArr,点击Add按钮可向数组中添加EditableComponent,随后将数组及setSomeArr通过props传入AllEditable组件渲染:
ParentComponent.js
import EditableComponent from "./components"; import AllEditable from "./components"; const ParentComponent = () => { const [someArr, setSomeArr] = useState([]); const handleNewEditable = () => { setSomeArr((prevContentArr) => { return [...prevContentArr, <EditableComponent />]; }); }; return ( <div className="wrapper"> <AllEditable someArr={someArr} setSomeArr={setSomeArr} /> <div> <button onClick={handleNewEditable}>Add</button> </div> </div> ); };
AllEditable组件中,每个渲染的EditableComponent旁有一个显示'X'的span,点击应删除对应的组件:
AllEditable.js
const AllEditable= (props) => { const deleteContentHandler = (index) => { props.setSomeArr((prevState) => { return prevState.filter((_, idx) => idx !== index); }); }; return ( <div> {props.someArr.map((content, idx) => { return ( <div key={`content-${idx}`}> <span onClick={() => {deleteContentHandler(idx);}}> X </span> <div>{content}</div> </div> ); })} </div> ); };
核心问题
无论点击哪个组件的删除按钮,总是会删除最后一个组件(开发者工具的Components面板中也呈现此现象)。删除逻辑(filter)本身是正确的——当移除contentEditable属性,并为每个组件添加唯一随机文本后,功能恢复正常。
已尝试的解决方法
- 创建不包含目标组件的新数组
- 将组件嵌套在带index键的对象中存入
someArr,例如:{idx: 0, content: <EditableComponent />} - 为每个
EditableComponent添加双击事件,切换contentEditable属性的true/false状态 - 将
EditableComponent中的元素替换为<textarea></textarea>替代<p contentEditable></p>
问题根源
问题出在用数组索引作为React元素的key,再结合contentEditable的特性导致的:
React会复用带有相同key的组件实例来优化渲染,而contentEditable元素的内容是直接在DOM层面修改的,React不会跟踪这些DOM变化。当删除中间元素后,数组索引重新排序,React会把原末尾组件的实例复用给新的索引位置,但DOM里的contentEditable内容还保留着原状态,视觉上就表现为“删除了最后一个组件”。
解决步骤
给每个新增元素生成唯一标识,替代索引作为key
不要直接存储组件实例,改为存储包含唯一id的对象结构。可以用uuid生成唯一id(需先安装npm install uuid):import { v4 as uuidv4 } from 'uuid'; const handleNewEditable = () => { setSomeArr((prevContentArr) => { return [...prevContentArr, { id: uuidv4(), component: <EditableComponent /> }]; }); };修改
AllEditable的渲染与删除逻辑
使用唯一id作为组件key,删除时通过id匹配目标元素:const AllEditable= (props) => { const deleteContentHandler = (itemId) => { props.setSomeArr((prevState) => { return prevState.filter(item => item.id !== itemId); }); }; return ( <div> {props.someArr.map((item) => { return ( <div key={item.id}> <span onClick={() => deleteContentHandler(item.id)}> X </span> <div>{item.component}</div> </div> ); })} </div> ); };无第三方包替代方案
不想引入uuid的话,可用时间戳+随机数生成唯一id:const handleNewEditable = () => { setSomeArr((prevContentArr) => { const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; return [...prevContentArr, { id: uniqueId, component: <EditableComponent /> }]; }); };
为什么移除contentEditable就正常?
没有contentEditable时,组件内容由React状态管理,即使复用组件实例,React也会同步更新内容与数据,不会出现视觉与逻辑不符的情况;而contentEditable绕过了React的状态控制,直接操作DOM,就暴露了key复用的问题。
内容的提问来源于stack exchange,提问作者001

