使用rows.splice()删除React表格行时总是删除最后一行的问题
问题描述
我有一个包含HTML表格的React组件,表格每行都配有react-select下拉框和删除本行的按钮。点击删除按钮时会传入选中行的索引,但删除最后一行功能正常,点击上方任意行(比如截图里值为“Test1”的行)时,却删掉了最后一行(值为“Test4”的行)——明明索引传的是对的,还是出现了这个问题。

对应的代码如下:
import { useEffect, useState } from "react"; import Select from "react-select"; function App(props) { const [state, setState] = useState({ rows: [ { config: "" } ] }); const [configOpts, setConfigOpts] = useState([]); const handleAddRow = () => { const newRow = { config: "" }; setState({ ...state, rows: [...state.rows, newRow] }); }; const handleRemoveRow = (idx) => () => { const rows = [...state.rows]; rows.splice(idx, 1); setState({ ...state, rows }); }; const handleConfigChange = (index) => (selectedOption) => { console.log("SelectedOption: " + selectedOption.label); const rows = [...state.rows]; rows[index] = { ...rows[index], ["config"]: selectedOption.label }; setState({ ...state, rows }); }; useEffect(() => { setConfigOpts([ { label: "Test1", value: "Test1" }, { label: "Test2", value: "Test2" }, { label: "Test3", value: "Test3" }, { label: "Test4", value: "Test4" } ]); }, []); return ( <> <table> {state.rows.map((item, idx) => ( <tr key={idx}> <td> <Select options={configOpts} onChange={handleConfigChange(idx)} /> </td> <td> <button onClick={handleRemoveRow(idx)}>Remove</button> </td> </tr> ))} </table> <div> <button onClick={handleAddRow}>Add</button> </div> </> ); } export default App;
问题原因及解决办法
核心原因
- 用数组索引作为
key导致的组件复用问题:React依赖key识别组件身份,当你用数组索引<tr key={idx}>时,删除中间行后,剩余行的索引会重新排列——比如删除索引0的行,原来索引1的行变成新的索引0,React会认为这是原来的索引0行,复用旧组件,导致绑定的删除事件还是旧的索引值,最终删错行。 - 闭包陷阱导致的旧state依赖:你的状态更新逻辑直接依赖
state快照,当组件重新渲染时,事件函数里的state还是旧值,也会引发错误。
解决步骤
1. 给每行添加唯一标识,替换索引作为key
给每个行对象添加唯一ID(比如时间戳),确保React能准确识别每一行:
- 修改初始化和添加行的逻辑:
const [state, setState] = useState({ rows: [ { id: Date.now(), // 添加唯一ID config: "" } ] }); const handleAddRow = () => { const newRow = { id: Date.now(), // 生成新的唯一ID config: "" }; setState(prev => ({ ...prev, rows: [...prev.rows, newRow] })); };
- 渲染时用行ID作为
key:
{state.rows.map((item) => ( <tr key={item.id}> {/* 其他内容不变 */} </tr> ))}
2. 改用函数式更新状态
所有修改state的操作,用函数式更新获取最新的state,避免闭包带来的旧值问题:
- 修改删除行逻辑:
const handleRemoveRow = (rowId) => () => { setState(prev => ({ ...prev, rows: prev.rows.filter(row => row.id !== rowId) })); }; // 调用时传行ID而非索引 <button onClick={handleRemoveRow(item.id)}>Remove</button>
- 修改下拉框选择逻辑:
const handleConfigChange = (rowId) => (selectedOption) => { console.log("SelectedOption: " + selectedOption.label); setState(prev => ({ ...prev, rows: prev.rows.map(row => row.id === rowId ? {...row, config: selectedOption.label} : row ) })); }; // 调用时传行ID而非索引 <Select options={configOpts} onChange={handleConfigChange(item.id)} />
完整修改后的代码
import { useEffect, useState } from "react"; import Select from "react-select"; function App(props) { const [state, setState] = useState({ rows: [ { id: Date.now(), config: "" } ] }); const [configOpts, setConfigOpts] = useState([]); const handleAddRow = () => { const newRow = { id: Date.now(), config: "" }; setState(prev => ({ ...prev, rows: [...prev.rows, newRow] })); }; const handleRemoveRow = (rowId) => () => { setState(prev => ({ ...prev, rows: prev.rows.filter(row => row.id !== rowId) })); }; const handleConfigChange = (rowId) => (selectedOption) => { console.log("SelectedOption: " + selectedOption.label); setState(prev => ({ ...prev, rows: prev.rows.map(row => row.id === rowId ? {...row, config: selectedOption.label} : row ) })); }; useEffect(() => { setConfigOpts([ { label: "Test1", value: "Test1" }, { label: "Test2", value: "Test2" }, { label: "Test3", value: "Test3" }, { label: "Test4", value: "Test4" } ]); }, []); return ( <> <table> {state.rows.map((item) => ( <tr key={item.id}> <td> <Select options={configOpts} onChange={handleConfigChange(item.id)} /> </td> <td> <button onClick={handleRemoveRow(item.id)}>Remove</button> </td> </tr> ))} </table> <div> <button onClick={handleAddRow}>Add</button> </div> </> ); } export default App;
内容的提问来源于stack exchange,提问作者SarangRN
相关产品推荐
相关产品推荐

