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

React.js:转react-table后遇更新深度超限错误求助

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 23:27:36