React TanStack Table输入框onBlur切换值异常问题求助
问题
参考TanStack Table的可编辑数据示例实现表格,包含“Current Value”可编辑列,基础功能正常,但使用Tab键导航表格时,单元格值会突然变化。尝试在onBlur事件中对比输入元素ID与行索引处理,未解决问题。代码如下:
import { RowData, ColumnDef, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import React, { useState, useEffect, useMemo } from "react"; import TestModeSelect from "./TestModeSelect"; import { Tooltip } from "flowbite-react"; declare module "@tanstack/react-table" { interface TableMeta<TData extends RowData> { updateData: (rowIndex: number, columnId: string, value: unknown) => void; } } const ConfigPanel: React.FC = () => { const [data, setData] = useState<MkDeviceCell[]>(() => []); const [testModeOptions, setTestModeOptions] = useState<MkDeviceTestMode[]>( [] ); const [quickModeOptions, setQuickModeOptions] = useState<MkDeviceQuickMode[]>( [] ); const readConfig = () => { getDeviceConfig() .then((result) => { setData(result.cells); setTestModeOptions(result.test_modes); setQuickModeOptions(result.quick_modes); }) .catch((err) => { error(`Error occurred while trying to read device config: ${err}`); }); }; const writeConfig = () => { setDeviceConfig(data).then((success) => { if (success) { readConfig(); } }); }; const factoryReset = async () => { let result = await ask( "This action will factory reset the device and cannot be reverted. Are you sure?", { title: "Tiny CC Tool", type: "warning", } ); if (result) { if (await invoke("factory_reset", {})) { await readConfig(); } } }; const handleCellValueChange = ( rowId: number, _columnId: string, value: string ) => { setData((old) => { return old.map((row) => { if (row.address === rowId) { row.current_value = parseInt(value); } return row; }); }); }; const columns = useMemo<ColumnDef<MkDeviceCell>[]>( () => [ { header: "Address", footer: (props) => props.column.id, accessorFn: (row) => row.address, id: "address", }, { header: "Name", footer: (props) => props.column.id, accessorFn: (row) => [row.name, row.description], id: "name", cell: ({ getValue }) => { const [name, description] = getValue() as [string, string]; return ( <> <Tooltip content={toolTipData(description)} placement="right" style="light" > {`${name}`} </Tooltip> </> ); }, }, { header: "Current Value", footer: (props) => props.column.id, accessorFn: (row) => [ row.current_value, row.min_value, row.max_value, row.allowed_values, row.address, ], id: "current_value", cell: ({ getValue, row: { index }, column: { id }, table }) => { const [initialValue, minValue, maxValue, allowedValues, address] = getValue() as [number, number, number, number[], number]; const [value, setValue] = useState(initialValue.toString()); const [activeValue, setActiveValue] = useState(""); // When the input is blurred, we'll call our table meta's updateData function const onBlur = (e: React.FocusEvent<HTMLInputElement>) => { const elemId = `ROW_${address}`; if (elemId !== e.target.id) { return; } const val = parseInt(value || ""); if (allowedValues.includes(val)) { table.options.meta?.updateData(index, id, val); } else if (val >= minValue && val <= maxValue) { table.options.meta?.updateData(index, id, val); } else { // Handle out-of-range values console.error( `Value ${val} is outside the allowed range (${minValue} - ${maxValue})` ); // You can add additional error handling or feedback to the user here } }; const onFocus = (e: React.FocusEvent<HTMLInputElement>) => { setActiveValue(e.target.value); }; const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log("On change"); setValue(e.target.value); }; // If the initialValue is changed external, sync it up with our state useEffect(() => { setValue(initialValue.toString()); }, [initialValue]); return ( <> <input id={`ROW_${address}`} key={`ROW_${address}`} value={value} onChange={handleOnChange} onBlur={onBlur} onFocus={onFocus} /> </> ); }, }, ], [] ); const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), meta: { updateData: (rowIndex, columnId, value) => { handleCellValueChange(rowIndex, columnId, value as string); }, }, debugTable: true, }); function toolTipData(description: string) { const array = description.split("\n"); return ( <div className="text-left"> {array.map((element, index) => ( <div key={index}> {element} <br /> </div> ))} </div> ); } function showTable(data: Array<object>) { if (data.length > 0) { return ( <table className="w-full text-center table border border-collapse "> <thead className="sticky top-0 bg-gray-50 text-center table-header-group border border-collapse "> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th key={header.id}> {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} </th> ))} </tr> ))} </thead> <tbody className="text-center "> {table.getRowModel().rows.map((row) => ( <tr key={row.id} className="border border-separate "> {row.getVisibleCells().map((cell) => ( <td key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ))} </tbody> </table> ); } else { return <div className="bg-gray-100 h-full"></div>; } } return ( <div className="h-full flex flex-col border rounded-lg"> <div className="overflow-y-scroll h-full">{showTable(data)}</div> <div className="p-2 bg-gray-50 border rounded-t-none rounded-lg sticky bottom-0 flex flex-row justify-between md:flex-wrap"> <div> <button onClick={() => readConfig()} className=" bg-blue-700 text-white text-xs p-2 border border-blue-950 rounded-l-lg hover:bg-blue-900 md:text-xs lg:text-lg" > Read Config </button> <button onClick={() => writeConfig()} className=" bg-blue-700 text-white border border-l-0 border-blue-950 text-xs p-2 hover:bg-blue-900 md:text-xs lg:text-lg" > Save Config </button> <button onClick={() => factoryReset()} className=" bg-blue-700 text-white text-xs border border-l-0 border-blue-950 rounded-r-lg p-2 hover:bg-blue-900 md:text-xs lg:text-lg" > Factory Reset </button> </div> <div className="float right md:mt-2"> <TestModeSelect testModeOptions={testModeOptions} quickOptions={quickModeOptions} /> </div> </div> </div> ); }; export default ConfigPanel;
解决方案
核心问题分析
- 行匹配逻辑错误:
updateData传入的是行索引(row.index),但handleCellValueChange中用row.address === rowId匹配行——行索引是表格内的位置序号,address是设备的唯一标识,两者语义完全不同,导致无法正确更新目标行数据,进而引发渲染异常。 - 单元格状态同步问题:每个单元格独立维护
value状态,且通过useEffect无条件监听initialValue(来自父组件data)。当其他单元格更新导致data变化时,所有单元格会重新渲染,useEffect会强制将value重置为initialValue,导致正在编辑的单元格输入内容丢失。
修复步骤
1. 修正行匹配逻辑
在单元格的onBlur事件中,传入设备address而非行索引到updateData,确保handleCellValueChange能正确匹配目标行:
// 在current_value列的cell组件中修改onBlur内的调用 table.options.meta?.updateData(address, id, val);
同时优化handleCellValueChange的参数命名,避免混淆:
const handleCellValueChange = ( deviceAddress: number, _columnId: string, value: string ) => { setData((old) => { return old.map((row) => { if (row.address === deviceAddress) { row.current_value = parseInt(value); } return row; }); }); };
2. 优化单元格状态管理
添加isEditing状态,仅在单元格未处于编辑状态时同步initialValue到value,避免编辑中被强制重置:
// 在current_value列的cell组件内修改 const [value, setValue] = useState(initialValue.toString()); const [isEditing, setIsEditing] = useState(false); const onBlur = (e: React.FocusEvent<HTMLInputElement>) => { setIsEditing(false); // ... 原有逻辑 }; const onFocus = (e: React.FocusEvent<HTMLInputElement>) => { setIsEditing(true); setActiveValue(e.target.value); }; // 仅在非编辑状态下同步initialValue useEffect(() => { if (!isEditing) { setValue(initialValue.toString()); } }, [initialValue, isEditing]);
完整修复后的核心代码片段
// 修正后的handleCellValueChange const handleCellValueChange = ( deviceAddress: number, _columnId: string, value: string ) => { setData((old) => { return old.map((row) => { if (row.address === deviceAddress) { row.current_value = parseInt(value); } return row; }); }); }; // 修正后的current_value列cell组件 cell: ({ getValue, column: { id }, table }) => { const [initialValue, minValue, maxValue, allowedValues, address] = getValue() as [number, number, number, number[], number]; const [value, setValue] = useState(initialValue.toString()); const [isEditing, setIsEditing] = useState(false); const [activeValue, setActiveValue] = useState(""); const onBlur = (e: React.FocusEvent<HTMLInputElement>) => { setIsEditing(false); const elemId = `ROW_${address}`; if (elemId !== e.target.id) { return; } const val = parseInt(value || ""); if (allowedValues.includes(val)) { table.options.meta?.updateData(address, id, val); } else if (val >= minValue && val <= maxValue) { table.options.meta?.updateData(address, id, val); } else { console.error( `Value ${val} is outside the allowed range (${minValue} - ${maxValue})` ); } }; const onFocus = (e: React.FocusEvent<HTMLInputElement>) => { setIsEditing(true); setActiveValue(e.target.value); }; const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); }; useEffect(() => { if (!isEditing) { setValue(initialValue.toString()); } }, [initialValue, isEditing]); return ( <input id={`ROW_${address}`} key={`ROW_${address}`} value={value} onChange={handleOnChange} onBlur={onBlur} onFocus={onFocus} /> ); },
内容的提问来源于stack exchange,提问作者technusm1
相关产品推荐
相关产品推荐

