React.js:转react-table后遇更新深度超限错误求助
问题现象
将代码迁移到react-table后触发如下错误:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
已尝试的排查操作:
- 删除所有
useState语句 - 移除所有包含
onClick/onChange的交互代码 - 仅保留
hoi、orderSystem等简单列
但问题仍未解决。
问题代码
// core packages import React, { useState, useEffect } from 'react'; import { useTable } from 'react-table'; import { useNavigate } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; // local imports import useCustomTable from '../../../utils/hooks/useTable'; import TableFooter from '../../components/table-footer/TableFooter'; import { ROWS_PER_PAGE } from '../../../utils/constants'; import { formatJsonString } from '../../../utils/helpers/helpers'; import PopupButton from '../../components/popup-button/PopupButton'; import { actions as productOrderActions } from '../../../store/productOrder'; import { selectProductOrder } from 'store/productOrder/selector'; // styles import './SearchResults.css'; const serviceNameMap = val => ( <tr key={val.serviceName + val.serviceAction + 'N'}> <td className="s1">{val.serviceName}</td> </tr> ); const serviceActionMap = val => ( <tr key={val.serviceName + val.serviceAction + 'A'}> <td className="s2">{val.serviceAction}</td> </tr> ); const serviceDueDateMap = val => ( <tr key={val.serviceName + val.serviceAction + 'D'}> <td className="s3"> {String(val.dueDate).substring(0, 10) === 'null' ? '' : String(val.dueDate).substring(0, 10)} </td> </tr> ); const SearchResults = ( props ) => { const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const isDict = dict => { return typeof dict === 'object' && !Array.isArray(dict); }; const [resDict, setResDict] = useState({}); const [page, setPage] = useState(1); const { slice, range } = useCustomTable( isDict(props.response) ? [props.response] : props.response, page, ROWS_PER_PAGE, ); const [productOrder, setProductOrder] = useState({ payload: '{}' }); const selectedProductOrder = useSelector(selectProductOrder); useEffect(() => { setProductOrder(selectedProductOrder); }, [selectedProductOrder]); const data = slice.map(val => { const len = val.orderDetails.length; const versionNames = []; for (let i = len; i > 0; i--) { versionNames.push(i); } const order = val.orderDetails[resDict[val.hoi] || 0]; return { hoi: order.hoi, orderSystem: order.orderSystem, brand: order.brand, status: order.status, serviceName: (<table cellSpacing="0"> <tbody> {order.serviceDetails.map(val => serviceNameMap(val))} </tbody> </table>), serviceAction: (<table cellSpacing="0"> <tbody> {order.serviceDetails.map(val => serviceActionMap(val))} </tbody> </table>), serviceDueDate: (<table cellSpacing="0"> <tbody> {order.serviceDetails.map(val => serviceDueDateMap(val))} </tbody> </table>), provisioning: order.provisioning, billing: order.billing, shipping: order.shipping, createDate: order.initialVersionCreateDate.substring(0, 10) + '\n' + order.initialVersionCreateDate.substring(11, 19), lastModified: order.currentVersionCreateDate.substring(0, 10) + '\n' + order.currentVersionCreateDate.substring(11, 19), version: ( <select className="version verSp" name="version" onChange={event => setResDict(() => ({ [order.hoi]: len - event.target.value }))} > {versionNames.map(versionName => ( <option name={versionName} key={versionName} value={versionName} > {versionName} </option> ))} </select> ), productOrder: ( <PopupButton onClick={() =>dispatch(productOrderActions.loadProductOrder({ENV: props.env, ORDER_NUMBER: order.hoi}))} buttonName={t('search_results.product_order')} title={t('search_results.product_order') + ': ' + order.hoi} content={formatJsonString(productOrder?.payload)} /> ), moreInfo: ( <button className="Button-Active" onClick={() => navigate('/orderInsightDetails', { state: { version: order.version, env: props.env, orderID: order.hoi }, }) } > {t('search_results.more_info')} </button> ), }; }); const columns = React.useMemo(() => [ { Header: t('search_results.hoi'), accessor: 'hoi', className: 'id', }, { Header: t('search_results.order_system'), accessor: 'orderSystem', className: 'orderSystem', }, { Header: t('search_results.brand'), accessor: 'brand', className: 'brand', }, { Header: t('search_results.status'), accessor: 'status', className: 'status', }, { Header: t('search_results.provisioning'), accessor: 'serviceName', className: 'serviceName', }, { Header: t('search_results.billing'), accessor: 'serviceAction', className: 'serviceAction', }, { Header: t('search_results.shipping'), accessor: 'serviceDueDate', className: 'serviceDueDate', }, { Header: t('search_results.provisioning'), accessor: 'provisioning', className: 'provisioning', }, { Header: t('search_results.billing'), accessor: 'billing', className: 'billing', }, { Header: t('search_results.shipping'), accessor: 'shipping', className: 'shipping', }, { Header: t('search_results.create_date'), accessor: 'createDate', className: 'createDate', }, { Header: t('search_results.last_modified'), accessor: 'lastModified', className: 'versionDate', }, { Header: t('search_results.version'), accessor: 'version', className: 'version', }, { Header: t('search_results.latest_product_order'), accessor: 'productOrder', className: 'info', }, { Header: t('search_results.more_info'), accessor: 'moreInfo', className: 'info', }, ] ); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns, data }); return ( <> {isDict(props.response) ? navigate('/notfoundpage') : <> <div className='scrollWrap'> <table {...getTableProps()} className='outerTable' cellSpacing='0'> <thead> {headerGroups?.map(headerGroup => ( <tr {...headerGroup?.getHeaderGroupProps()}> {headerGroup?.headers?.map(column => ( <th {...column?.getHeaderProps()}>{column?.render('Header')}</th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows?.map(row => { prepareRow(row); return ( <tr {...row?.getRowProps()} className='mainRow'> {row?.cells?.map(cell => ( <td className={cell.column.className} {...cell.getCellProps()}> {cell.render('Cell')} </td> ))} </tr> ); })} </tbody> </table> </div> <TableFooter range={range} page={page} setPage={setPage}></TableFooter> </> } </> ); }; export default SearchResults;
问题根源及修复方案
1. columns 未添加useMemo依赖数组
当前columns的useMemo没有传入依赖数组,每次组件渲染都会生成新的数组引用。react-table检测到columns变化后,会触发表格状态重新计算,进而导致组件重复渲染,形成无限循环。
修复:
给useMemo添加依赖数组,包含翻译函数t:
const columns = React.useMemo(() => [ // 原列配置内容不变 ], [t]);
2. data 未做缓存处理
data是每次渲染时通过slice.map生成的新数组,且内部包含React元素(如serviceName、version等),每次渲染都会生成新的引用,导致react-table不断重新计算,触发组件更新。
修复:
用useMemo缓存data,添加必要的依赖数组:
const data = React.useMemo(() => { return slice.map(val => { // 原map逻辑内容不变 }); }, [slice, resDict, productOrder, t, props.env, navigate, dispatch]);
3. 渲染阶段直接调用navigate
在组件渲染阶段调用navigate会触发路由跳转,导致组件卸载后重新渲染,可能引发循环。应改用useEffect处理路由跳转:
useEffect(() => { if (isDict(props.response)) { navigate('/notfoundpage'); } }, [props.response, isDict, navigate]); // 渲染部分修改为: return ( <> {!isDict(props.response) && ( <> <div className='scrollWrap'> {/* 原表格代码 */} </div> <TableFooter range={range} page={page} setPage={setPage}></TableFooter> </> )} </> );
4. isDict 函数重复创建
每次渲染都会重新创建isDict函数,可能导致依赖它的代码不必要更新,建议用useCallback缓存:
const isDict = React.useCallback(dict => { return typeof dict === 'object' && !Array.isArray(dict); }, []);
5. 检查useCustomTable内部实现
如果useCustomTable内部没有用useMemo缓存slice和range,每次渲染返回新的引用,也会导致组件更新。需确保useCustomTable内部对返回值做缓存,依赖数组包含传入的data、page、rowsPerPage。
总结
columns未添加依赖数组和data未做缓存是导致无限循环的核心原因,优先修复这两处即可解决问题。
内容的提问来源于stack exchange,提问作者Nishi

