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

React应用行内编辑需两次保存才生效的问题修复请求

React行内编辑保存后UI不即时更新问题修复

问题描述

我用React构建了一个数据展示应用,通过API拉取数据并渲染到表格,支持本地行内编辑和删除操作(仅修改本地存储,不改动API原始数据)。当前环境Node.js 18.12.1、npm 10.2.3。遇到的问题:

  • 点击编辑按钮进入行内编辑状态,修改数据后点击保存,变更无法立即在UI上显示,需再次进入编辑并保存才会生效
  • 页面刷新后会恢复显示API的原始数据

以下是我的代码:

App.js

import React, { useState, useEffect } from 'react';
import Table from './table';
import './App.css';
import axios from 'axios';

const API_ENDPOINT = 'https://geektrust.s3-ap-southeast-1.amazonaws.com/adminui-problem/members.json';

const App = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fun() {
      const res = await axios.get(API_ENDPOINT);
      setData(res.data);
    }
    fun();
  }, []);

  return (
    <div>
      <Table data={data} />
    </div>
  );
};

export default App;

Table.js

import React, { useState, useEffect } from 'react';
import TableRow from './tableRow';
import './table.css';

const Table = ({ data }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedRows, setSelectedRows] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [isEditing, setEditing] = useState(false);
  const [editedRowData, setEditedRowData] = useState({});
  const rowsPerPage = 5;

  const [visibleData, setVisibleData] = useState(data);

  useEffect(() => {
    const filteredData = data
      .filter(row =>
        Object.values(row).some(value =>
          value.toString().toLowerCase().includes(searchTerm.toLowerCase())
        )
      );
    setVisibleData(filteredData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage));
  }, [data, searchTerm, currentPage, rowsPerPage]);

  const totalPages = Math.ceil(data.length / rowsPerPage);
  const paginationButtons = Array.from({ length: totalPages }, (_, index) => index + 1);

  const handleEdit = (editedRow) => {
    setEditedRowData(editedRow);
    setEditing(true);
  };

  const handleSave = () => {
    const updatedData = data.map(row => (row.id === editedRowData.id ? editedRowData : row));
    setVisibleData(updatedData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage));
    setEditing(false);
  };

  const handleDelete = () => {
    const updatedData = data.filter(row => !selectedRows.includes(row.id));
    setVisibleData(updatedData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage));
    setSelectedRows([]);
  };

  const handlePageChange = (page) => {
    setCurrentPage(page);
  };

  const handleToggleAllRows = () => {
    const allRows = visibleData.map(row => row.id);
    if (selectedRows.length === allRows.length) {
      setSelectedRows([]);
    } else {
      setSelectedRows(allRows);
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <table>
        <thead>
          <tr>
            <th>
              <input
                type="checkbox"
                checked={selectedRows.length === visibleData.length}
                onChange={handleToggleAllRows}
              />
            </th>
            {Object.keys(data[0] || {}).map((column) => (
              <th key={column}>{column}</th>
            ))}
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {visibleData.map((row) => (
            <TableRow
              key={row.id}
              row={row}
              selectedRows={selectedRows}
              setSelectedRows={setSelectedRows}
              isEditing={isEditing}
              onEdit={handleEdit}
              onSave={handleSave}
            />
          ))}
        </tbody>
      </table>
      <div className="pagination">
        {paginationButtons.map((button) => (
          <button
            key={button}
            onClick={() => handlePageChange(button)}
            className={button === currentPage ? 'active' : ''}
          >
            {button}
          </button>
        ))}
      </div>
      <button className="delete-selected" onClick={handleDelete}>
        Delete Selected
      </button>
    </div>
  );
};

export default Table;

TableRow.js

import React, { useState } from 'react';

const TableRow = ({ row, selectedRows, setSelectedRows, isEditing, onEdit, onSave }) => {
  const [editedValues, setEditedValues] = useState({ ...row });
  const isSelected = selectedRows.includes(row.id);

  const handleEdit = (tempRow) => {
    onEdit(tempRow);
  };

  const handleSave = () => {
    onSave();
  };

  const handleToggleRow = () => {
    setSelectedRows(prevSelectedRows => {
      if (prevSelectedRows.includes(row.id)) {
        return prevSelectedRows.filter(id => id !== row.id);
      } else {
        return [...prevSelectedRows, row.id];
      }
    });
  };

  const handleChange = (column, value) => {
    setEditedValues(prevEditedValues => ({
      ...prevEditedValues,
      [column]: value,
    }));
    console.log("edited values:",editedValues);
  };

  return (
    <tr className={isSelected ? 'selected' : ''}>
      <td>
        <input
          type="checkbox"
          checked={isSelected}
          onChange={handleToggleRow}
        />
      </td>
      {Object.keys(row).map(column => (
        <td key={column}>
          {isEditing ? (
            <input
              type="text"
              value={editedValues[column]}
              onChange={(e) => handleChange(column, e.target.value)}
            />
          ) : (
            row[column]
          )}
        </td>
      ))}
      <td>
        {isEditing ? (
          <button onClick={() => handleSave(editedValues)}>Save</button>
        ) : (
          <button onClick={() => handleEdit(editedValues)}>Edit</button>
        )}
      </td>
      <td>
        {!isEditing ? (
          <button className="delete" onClick={() => setSelectedRows([row.id])}>
            Delete
          </button>
        ) : null}
      </td>
    </tr>
  );
};

export default TableRow;

问题原因分析

  1. 编辑值传递缺失:TableRow点击保存时传递了最新的editedValues,但Table组件的handleSave没有接收这个参数,导致更新用的是编辑初始时的旧数据editedRowData。
  2. 源头数据未更新:App组件的data是原始数据源,Table组件仅更新了自己的visibleData,没有同步更新App的data,导致页面刷新后恢复原始数据;同时依赖data的useEffect会在搜索、分页时覆盖visibleData的修改。
  3. 全局编辑状态混乱:用单个isEditing布尔值控制所有行的编辑状态,会导致所有行同时进入编辑模式,且无法精准定位当前编辑的行。

修复方案及代码修改

1. App.js:添加数据更新回调

将数据更新逻辑提升到App组件,Table通过回调修改源头数据:

import React, { useState, useEffect } from 'react';
import Table from './table';
import './App.css';
import axios from 'axios';

const API_ENDPOINT = 'https://geektrust.s3-ap-southeast-1.amazonaws.com/adminui-problem/members.json';

const App = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fun() {
      const res = await axios.get(API_ENDPOINT);
      setData(res.data);
    }
    fun();
  }, []);

  // 提供给Table的更新数据回调
  const updateData = (updatedRows) => {
    setData(updatedRows);
  };

  return (
    <div>
      <Table data={data} updateData={updateData} />
    </div>
  );
};

export default App;

2. Table.js:优化状态管理与数据同步

  • 将全局isEditing改为editingRowId,记录当前编辑的行ID
  • 修改handleSave接收最新编辑值,通过updateData同步到App的数据源
  • 修正删除逻辑,同步更新源头数据
import React, { useState, useEffect } from 'react';
import TableRow from './tableRow';
import './table.css';

const Table = ({ data, updateData }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedRows, setSelectedRows] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [editingRowId, setEditingRowId] = useState(null); // 记录当前编辑的行ID
  const rowsPerPage = 5;

  const [visibleData, setVisibleData] = useState(data);

  useEffect(() => {
    const filteredData = data
      .filter(row =>
        Object.values(row).some(value =>
          value.toString().toLowerCase().includes(searchTerm.toLowerCase())
        )
      );
    setVisibleData(filteredData.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage));
  }, [data, searchTerm, currentPage, rowsPerPage]);

  const totalPages = Math.ceil(data.length / rowsPerPage);
  const paginationButtons = Array.from({ length: totalPages }, (_, index) => index + 1);

  const handleEdit = (rowId) => {
    setEditingRowId(rowId);
  };

  const handleSave = (updatedRow) => {
    // 更新源头数据
    const updatedData = data.map(row => 
      row.id === updatedRow.id ? updatedRow : row
    );
    updateData(updatedData);
    setEditingRowId(null); // 退出编辑状态
  };

  const handleDelete = () => {
    const updatedData = data.filter(row => !selectedRows.includes(row.id));
    updateData(updatedData);
    setSelectedRows([]);
  };

  const handlePageChange = (page) => {
    setCurrentPage(page);
    setEditingRowId(null); // 切换页面时退出编辑状态
  };

  const handleToggleAllRows = () => {
    const allRows = visibleData.map(row => row.id);
    if (selectedRows.length === allRows.length) {
      setSelectedRows([]);
    } else {
      setSelectedRows(allRows);
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <table>
        <thead>
          <tr>
            <th>
              <input
                type="checkbox"
                checked={selectedRows.length === visibleData.length}
                onChange={handleToggleAllRows}
              />
            </th>
            {Object.keys(data[0] || {}).map((column) => (
              <th key={column}>{column}</th>
            ))}
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {visibleData.map((row) => (
            <TableRow
              key={row.id}
              row={row}
              selectedRows={selectedRows}
              setSelectedRows={setSelectedRows}
              isEditing={editingRowId === row.id} // 判断当前行是否处于编辑状态
              onEdit={() => handleEdit(row.id)}
              onSave={handleSave}
            />
          ))}
        </tbody>
      </table>
      <div className="pagination">
        {paginationButtons.map((button) => (
          <button
            key={button}
            onClick={() => handlePageChange(button)}
            className={button === currentPage ? 'active' : ''}
          >
            {button}
          </button>
        ))}
      </div>
      <button className="delete-selected" onClick={handleDelete}>
        Delete Selected
      </button>
    </div>
  );
};

export default Table;

3. TableRow.js:修正编辑状态判断与值传递

  • 根据isEditing(当前行是否为编辑行)控制显示
  • 点击保存时传递最新的editedValues给Table的handleSave
import React, { useState, useEffect } from 'react';

const TableRow = ({ row, selectedRows, setSelectedRows, isEditing, onEdit, onSave }) => {
  // 编辑状态切换时重置编辑值
  const [editedValues, setEditedValues] = useState({ ...row });
  
  useEffect(() => {
    if (isEditing) {
      setEditedValues({ ...row });
    }
  }, [isEditing, row]);

  const isSelected = selectedRows.includes(row.id);

  const handleToggleRow = () => {
    setSelectedRows(prevSelectedRows => {
      if (prevSelectedRows.includes(row.id)) {
        return prevSelectedRows.filter(id => id !== row.id);
      } else {
        return [...prevSelectedRows, row.id];
      }
    });
  };

  const handleChange = (column, value) => {
    setEditedValues(prev => ({
      ...prev,
      [column]: value,
    }));
  };

  return (
    <tr className={isSelected ? 'selected' : ''}>
      <td>
        <input
          type="checkbox"
          checked={isSelected}
          onChange={handleToggleRow}
        />
      </td>
      {Object.keys(row).map(column => (
        <td key={column}>
          {isEditing ? (
            <input
              type="text"
              value={editedValues[column]}
              onChange={(e) => handleChange(column, e.target.value)}
            />
          ) : (
            row[column]
          )}
        </td>
      ))}
      <td>
        {isEditing ? (
          <button onClick={() => onSave(editedValues)}>Save</button>
        ) : (
          <button onClick={onEdit}>Edit</button>
        )}
      </td>
      <td>
        {!isEditing ? (
          <button className="delete" onClick={() => {
            setSelectedRows([row.id]);
          }}>
            Delete
          </button>
        ) : null}
      </td>
    </tr>
  );
};

export default TableRow;

修复效果说明

  • 编辑保存后,UI会即时更新显示修改后的数据
  • 页面刷新前的本地修改会保持(若需持久化可添加localStorage逻辑)
  • 仅当前点击的行会进入编辑状态,避免全局状态混乱
  • 搜索、分页操作会基于最新的本地数据进行

内容的提问来源于stack exchange,提问作者amar kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 20:24:51