如何更新React三层嵌套状态并避免表格全量重渲染
问题场景
需要更新三层深度嵌套的React状态,目标是修改企业数据下对应交易条目的金额字段,避免更新状态时触发整个表格重渲染,且不引入额外状态管理库。
现有状态定义
const [companies, setCompanies] = useState(companies)
嵌套数据结构
companies为数组,存储多家企业信息,单企业数据结构示例:
const companies = [ { companyId: 100, transactions: [ { id: "10421A", amount: "850", }, { id: "1893B", amount: "357", } ] } ]
现有表格组件代码
基于react-table实现的表格组件如下:
function DataTable({ editCell, vendors, accounts }) { const columns = useMemo(() => table.columns, [table]); const data = useMemo(() => table.rows, [table]); const tableInstance = useTable({ columns, data, initialState: { pageIndex: 0 } }, useGlobalFilter, useSortBy, usePagination); const { getTableProps, getTableBodyProps, headerGroups, prepareRow, rows, page, state: { pageIndex, pageSize, globalFilter }, } = tableInstance; return ( <Table {...getTableProps()}> <MDBox component="thead"> {headerGroups.map((headerGroup) => ( <TableRow {...headerGroup.getHeaderGroupProps()}> {headerGroups.headers.map((column) => ( <DataTableHeadCell {...column.getHeaderProps(isSorted && column.getSortByToggleProps())} width={column.width ? column.width : "auto"} align={column.align ? column.align : "left"} sorted={setSortedValue(column)} > {column.render("Header")} </DataTableHeadCell> ))} </TableRow> ))} </MDBox> <TableBody {...getTableBodyProps()}> {page.map((row, key) => { prepareRow(row); return ( <TableRow {...row.getRowProps()}> {row.cells.map((cell) => { cell.itemsSelected = itemsSelected; cell.editCell = editCell; cell.vendors = vendors; cell.accounts = accounts; return ( <DataTableBodyCell noBorder={noEndBorder && rows.length - 1 === key} align={cell.column.align ? cell.column.align : "left"} {...cell.getCellProps()} > {cell.render("Cell")} </DataTableBodyCell> ); })} </TableRow> ); })} </TableBody> </Table> ) }
当前实现问题:更新状态时全量替换companies数组会触发全表格重渲染,网上现有方案多针对具名对象属性的嵌套场景,不适配当前数组嵌套结构。
实现方案
核心逻辑是最小粒度不可变更新+组件记忆化,不需要引入第三方库,也不需要修改React状态的基本规则。
1. 编写最小粒度的状态更新方法
更新状态时不要深拷贝全量数据,仅拷贝更新路径上的父节点,所有未涉及修改的节点保持原有引用不变。
以更新指定企业下指定交易的amount字段为例,更新函数如下:
// 入参:目标企业ID、目标交易ID、新的金额值 const updateTransactionAmount = (targetCompanyId, targetTransactionId, newAmount) => { setCompanies(prev => prev.map(company => { // 非目标企业直接返回原对象,引用不变 if (company.companyId !== targetCompanyId) return company; // 目标企业返回浅拷贝的新对象,仅更新transactions字段 return { ...company, transactions: company.transactions.map(transaction => { // 非目标交易直接返回原对象,引用不变 if (transaction.id !== targetTransactionId) return transaction; // 目标交易返回浅拷贝的新对象,更新amount字段 return { ...transaction, amount: newAmount } }) } })) }
该更新方式只会生成3个新引用:目标企业对象、目标企业的transactions数组、目标交易对象,其余所有未修改的企业、交易条目都保持原有引用,React浅比较时会自动跳过引用未变化的节点。
2. 给表格行、单元格组件加记忆化
- 用
React.memo包裹TableRow、DataTableBodyCell组件,组件会自动对传入的props做浅比较,只有props引用发生变化时才会重渲染。 - 移除现有代码中render阶段直接修改cell对象的写法(即
cell.itemsSelected = itemsSelected这类直接给cell挂载属性的逻辑),这类写法会在每次父组件渲染时修改所有cell的属性,破坏引用稳定性,导致不必要的重渲染。需要传给单元格的参数可以通过列定义的Cell渲染函数传入,或通过getCellProps方法返回,保证props引用稳定。
3. 保证表格数据源的引用稳定
确保useTable传入的data、columns配置都正确配置了useMemo依赖,只有当实际数据变化时才返回新的引用。结合第一步的最小粒度更新,未修改的行、单元格对应的数据引用不会变化,被memo包裹的组件就不会触发重渲染,最终只有实际修改了值的单元格会重渲染。
禁止使用深拷贝方法(如
JSON.parse(JSON.stringify())、lodash.cloneDeep)更新状态,这类方法会生成所有节点的新引用,必然导致全表重渲染,完全抵消优化效果。
补充说明
数组嵌套场景和具名对象属性场景的不可变更新逻辑没有本质区别:遍历数组时非目标项直接返回原引用,目标项浅拷贝后更新对应字段即可,不需要特殊语法或第三方库。
不存在「直接修改嵌套值不触发父状态更新」的合法方案,React状态更新必须遵循不可变原则,只需要控制更新时的引用变化范围,就能实现局部重渲染。
内容的提问来源于stack exchange,提问作者Mustafa Yusuf

