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

如何为进行数据请求的react-table组件包裹Error Boundary并解决无限重渲染问题

问题描述

此前参照react-table官方文档中数据请求相关指南开发,在给Table组件包裹Error Boundary时遇到如下报错:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

未基于筛选、分页条件拉取数据时没有出现该问题,在Table组件中引入useEffect方法后,由于fetchData方法会修改columns、筛选条件、表格数据等状态,导致出现无限重渲染的问题。

相关代码

外层ErrorBoundary包裹逻辑

<ErrorBoundary  key={generateStringId()} fallbackRender={props =>
    <ErrorFallback {...props} error_code={PAGE_LOAD}/>
}>
    <Table
        columns={columns}
        data={data}
        fetchData={fetchData}
        status={status}
        pageCount={pageCount}
        availableFilters={availableFilters}
        activeFilters={activeFilters}
    />
</ErrorBoundary>

Table组件核心代码

import React, {useEffect, useState, useCallback} from 'react';
import {useSortBy,usePagination,useTable,useRowSelect} from "react-table";
import {ErrorBoundary} from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";
import SearchFilterBox from './sub_components/SearchFilterBox'
import {DATATABLE} from "../ErrorCodes";
import "./../css/datatables.scss";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";

function Table({
                   columns,
                   data,
                   fetchData,
                   loading,
                   clientId,
                   orgId,
                   updateSettingData,
                   updateCellData,
                   deleteCellData,
                   activeFilters,
                   skipPageReset,
                   availableFilters = {},
                   tableSettings = {},
                   useFilters = true,
                   displayPagination = true,
                   displayFooter = false,
                   displayRowSelect = false,
                   rowSelectAction,
                   rowSelectTitle = '',
                   hiddenColumns= ["metadata"],
                   pageCount: controlledPageCount,
                   paginationOptions = [10,25,50,100],
                   contentBesideFilters,
               }) {
    const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        footerGroups,
        prepareRow,
        page,
        canPreviousPage,
        canNextPage,
        pageOptions,
        pageCount,
        gotoPage,
        nextPage,
        previousPage,
        setPageSize,
        initialState,
        selectedFlatRows,
        state: { pageIndex, pageSize, sortBy},
    } = useTable(
        {
            columns,
            data,
            availableFilters,
            updateSettingData,
            updateCellData,
            deleteCellData,
            autoResetPage: !skipPageReset,
            initialState: {
                pageIndex: 0,
                hiddenColumns: 'metadata',
                sortBy: [{
                    id: tableSettings['defaultSort'],
                    desc: false
                }],
            },
            manualPagination: true,
            pageCount: controlledPageCount,
            manualSortBy: true,
        },
        useSortBy,
        usePagination,
    );

    const [filter, setFilter] = useState({
        search : {
            term : '',
            fields : ''
        },
        dateRange: {
            field: '',
            start: '',
            end: '',
            period: ''
        },
        sort: '',
    });


    if (availableFilters.search !== undefined) {
        console.log("Filters Defined!");
        if(availableFilters.search.columns.length <= 0) {
            console.log("useFilter set to False!")
            useFilters = false;
        }
    }

    useEffect(() => {
        let dataFilter = {
            ...filter,
            sort: sortBy,
        };

        if (typeof fetchData === 'function') {
            fetchData({pageIndex, pageSize, dataFilter, clientId, orgId});

        }

    }, [fetchData, pageIndex, pageSize, filter, sortBy, clientId, orgId]);


    const updateFilters = useCallback((filters) => {
        setFilter(filters);
    });

    return (
        <>
            <div className="filterContainer">
            <ErrorBoundary
                fallbackRender={props => <ErrorFallback {...props} error_code={DATATABLE}/>}
            >


            <SearchFilterBox
                availableFilters={availableFilters}
                activeFilters={activeFilters}
                onChange={filters => updateFilters(filters)}
                contentBesideFilters={contentBesideFilters}
                settings={tableSettings}
                useFilters={useFilters}
            />


            <table {...getTableProps()}>
                <thead>
                {headerGroups.map(headerGroup => (
                    <tr {...headerGroup.getHeaderGroupProps()}>
                        {headerGroup.headers.map(column => (
                            <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                                {column.render('Header')} {filter.search.fields.includes(column.id)
                                ? <FontAwesomeIcon icon={['fas', 'search']} className={"searchable"}/>
                                : ''}
                                <span>
                    {column.isSorted
                        ? column.isSortedDesc
                            ? <FontAwesomeIcon icon={['fa','caret-up']}/>
                            : <FontAwesomeIcon icon={['fa','caret-down']}/>
                        : ''}
                  </span>
                            </th>
                        ))}
                    </tr>
                ))}
                </thead>
                <tbody {...getTableBodyProps()}>
                {page.map((row, i) => {
                    prepareRow(row);

                    return (
                        <tr {...row.getRowProps()}>
                            {row.cells.map(cell => {
                                let mobileRowName = cell.getCellProps().key.split("_");
                                let classes = mobileRowName[2] === 'Actions' ? 'actionsCol' : '';
                                return <td className={classes} data-label={mobileRowName[2]} {...cell.getCellProps()}>{cell.render('Cell')}</td>
                            })}
                        </tr>
                    )
                })}

                </tbody>
                <tfoot>
                {
                    displayFooter &&
                    footerGroups &&
                    footerGroups.map(group => (
                        <tr {...group.getFooterGroupProps()} style={{"fontWeight":"bold"}}>
                            {group.headers.map(column => (
                                <td {...column.getFooterProps()}>{column.render('Footer')}</td>
                            ))}
                        </tr>
                    ))
                }
                {
                    displayPagination &&
                        (loading ?
                            (<tr><td colSpan="10000">Loading...</td></tr>) :
                            (<tr><td colSpan="10000">
                                Showing {page.length} of about {controlledPageCount * pageSize}{' '}
                                results
                            </td></tr>)
                        )
                }
                </tfoot>
            </table>
                {
                    displayRowSelect &&
                    <button
                        className={`download-documents btn-${selectedFlatRows.length ===0 ?'disabled':'primary'}`}
                        disabled={selectedFlatRows.length === 0}
                        onClick={() => rowSelectAction(selectedFlatRows)}>
                        {rowSelectTitle}
                    </button>
                }
            {displayPagination &&
            <div className="pagination">
                <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
                    <FontAwesomeIcon icon={['fas','angle-double-left']}/>
                </button>{' '}
                <button onClick={() => previousPage()} disabled={!canPreviousPage}>
                    <FontAwesomeIcon icon={['fas','angle-left']}/>
                </button>{' '}
                <button onClick={() => nextPage()} disabled={!canNextPage}>
                    <FontAwesomeIcon icon={['fas','angle-right']}/>
                </button>{' '}
                <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
                    <FontAwesomeIcon icon={['fas','angle-double-right']}/>
                </button>{' '}
                <span>
          Page{' '}
                    <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
                <span>
          | Go to page:{' '}
                    <input
                        type="number"
                        defaultValue={pageIndex + 1}
                        onChange={e => {
                            const page = e.target.value ? Number(e.target.value) - 1 : 0
                            gotoPage(page)
                        }}
                        style={{ width: '100px' }}
                    />
        </span>{' '}
                { paginationOptions.length > 1 &&<select
                    value={pageSize}
                    onChange={e => {
                        setPageSize(Number(e.target.value))
                    }}
                >
                    {paginationOptions.map(pageSize => (
                        <option key={pageSize} value={pageSize}>
                            Show {pageSize}
                        </option>
                    ))}
                </select>}
            </div>
            }
            </ErrorBoundary>
            </div>
        </>
    )
}

export default React.memo(Table);
修复方案
  • 移除ErrorBoundary上的动态key:key={generateStringId()}会导致每次父组件渲染都生成新的key值,触发ErrorBoundary完全重新挂载,内部Table组件随之重新初始化,触发useEffect执行后调用fetchData更新父组件状态,进入死循环。无特殊重置需求不要给ErrorBoundary设置动态key。
  • 稳定fetchData的引用:父组件定义fetchData时使用useCallback包裹,正确填写依赖项,保证每次渲染返回的函数引用一致,避免触发useEffect不必要的执行。如果不需要监听fetchData的变化,也可以将fetchData存入useRef,在useEffect中通过ref.current调用,无需将其放入依赖数组。
  • 补全useCallback的依赖项:Table组件中的updateFilters的useCallback未填写依赖数组,每次渲染都会生成新的函数引用,导致子组件SearchFilterBox不必要的重渲染。修改为:
const updateFilters = useCallback((filters) => {
    setFilter(filters);
}, []);
  • 稳定传入Table的配置项引用:父组件传给Table的columns、availableFilters等配置项,如果是每次渲染生成的新对象,会触发Table重渲染。这类配置可以提到组件外部定义,或者使用useMemo包裹保证引用稳定。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 19:06:04