React点击触发动态列表按rank排序时修改输入导致数组覆盖问题
问题描述
渲染前尝试对participants数组按rank字段降序排序,但修改、新增数组值时会出现覆盖数组内后续对象的异常。
复现代码
示例participants数组
const participants= [ {rank: 15, name: 'Alex'}, {rank: 5, name: 'Eric'}, {rank: 16, name: 'Jon'}, {rank: 20, name: 'Zach'}, ]
renderRows方法实现
renderRows() { var context = this; const sortedParticipants = [...this.state.participants].sort((a, b) => b.rank - a.rank); console.log(sortedParticipants); // 控制台可确认sortedParticipants已完成排序 // return [...this.state.participants].sort((a, b) => b.rank - a.rank).map(function(o, i) { // 备选返回写法 return this.state.participants.map(function(o, i) { /* 使用this.state.sortedParticipants会渲染出空白页面 */ return ( <tr key={'participants-' + i}> <td> <input id='rank' type='text' value={o.rank} onChange={context.handleParticipantChanged.bind(context, i)} /> </td> <td> <input id='name' type='text' value={o.name} onChange={context.handleParticipantChanged.bind(context, i)} /> </td> <td> <button onClick={context.handleParticipantDeleted.bind(context, i)}> Delete </button> </td> </tr> ); }); }
handleParticipantChanged方法实现
handleParticipantChanged(i, event) { var participants = this.state.participants; participants[i] = event.target.value; this.setState({ participants: participants }); }
异常表现
- 控制台日志可确认
sortedParticipants已按预期排序,但使用return this.state.sortedParticipants.map(...)写法时页面渲染为空白 - 使用注释中的备选返回写法(直接对排序后的数组调用map)时,基础渲染正常,但修改任意输入值时,旧值会错位覆盖对应索引的后续元素:
- 初始排序后数组前3个元素为:
0: Object { rank: "16", name: "Jon" } 1: Object { rank: "15", name: "Alex" } 2: Object { rank: "5", name: "Eric" }
- 将第一个对象修改为
20, Zach时,索引0的对象会覆盖索引1的对象,异常输出:
0: "Zach" 1: Object { rank: "16", name: "Jon" } 2: Object { rank: "5", name: "Eric" }
- 再向索引1位置输入
15, Alex时,当前数据会继续覆盖下一个索引的对象:
0: "Zach" 1: "Alex" 2: Object { rank: "16", name: "Jon" }
- 新增第5个参与者(
18, Jeff)时,异常输出:
0: "Zach" 1: "Alex" 2: Object { rank: "18", name: "Jeff" } 3: Object { rank: "16", name: "Jon" }
问题根因
- 不存在的状态引用:
sortedParticipants是renderRows方法内定义的局部变量,从未被存入this.state,因此this.state.sortedParticipants值为undefined,调用map方法会直接报错,导致页面空白。 - 索引错位:渲染时临时排序生成了新的数组顺序,但事件回调绑定的索引是排序后数组的索引,和原始
this.state.participants的存储顺序不匹配,修改时用排序后的索引操作原始顺序的状态数组,必然出现数据错位。 - 更新逻辑错误:
handleParticipantChanged直接把数组对应索引的项整体赋值为event.target.value(输入框的字符串值),既没有区分修改的是rank还是name字段,也没有遵循React不可变更新原则,直接修改原状态数组引用,导致数据结构被破坏,出现字符串替换对象的异常。 - key值错误:使用数组索引作为列表项的key,排序后索引顺序变化,React虚拟DOM diff时会错误复用已有输入框的DOM节点,进一步加重输入值错位问题。
修复方案
- 给每个参与者数据添加全局唯一的固定id,不要依赖数组索引做数据匹配
- 渲染时可直接对原数组做临时排序,不需要把排序结果存入state
- 事件回调传参使用数据唯一id而非数组索引,更新时通过id匹配要修改的项,遵循不可变更新原则,仅修改对应字段,不要把整个数据项替换为输入字符串
- 列表项使用唯一id作为key,避免DOM复用错误
修复后核心代码
- 初始化数组时添加唯一id:
const participants= [ {id: 1, rank: 15, name: 'Alex'}, {id: 2, rank: 5, name: 'Eric'}, {id: 3, rank: 16, name: 'Jon'}, {id: 4, rank: 20, name: 'Zach'}, ]
- 修正更新方法:
handleParticipantChanged(id, field, event) { // 复制原数组生成新引用,不直接修改原状态 const newParticipants = this.state.participants.map(item => { if (item.id === id) { // 仅更新对应字段,不替换整个对象 return { ...item, [field]: event.target.value } } return item }) this.setState({ participants: newParticipants }); }
- 修正渲染逻辑:
renderRows() { const sortedParticipants = [...this.state.participants].sort((a, b) => b.rank - a.rank); return sortedParticipants.map((o) => { return ( <tr key={'participants-' + o.id}> <td> <input type='text' value={o.rank} // 传入当前项id和字段名,不传排序后的索引 onChange={this.handleParticipantChanged.bind(this, o.id, 'rank')} /> </td> <td> <input type='text' value={o.name} onChange={this.handleParticipantChanged.bind(this, o.id, 'name')} /> </td> <td> <button onClick={this.handleParticipantDeleted.bind(this, o.id)}> Delete </button> </td> </tr> ); }); }
- 修正删除方法(按id过滤):
handleParticipantDeleted(id) { const newParticipants = this.state.participants.filter(item => item.id !== id) this.setState({participants: newParticipants}) }
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

