如何为进行数据请求的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
相关产品推荐
相关产品推荐

