You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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;
问题原因及解决办法

核心原因

  1. 用数组索引作为key导致的组件复用问题:React依赖key识别组件身份,当你用数组索引<tr key={idx}>时,删除中间行后,剩余行的索引会重新排列——比如删除索引0的行,原来索引1的行变成新的索引0,React会认为这是原来的索引0行,复用旧组件,导致绑定的删除事件还是旧的索引值,最终删错行。
  2. 闭包陷阱导致的旧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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 01:05:44