React组件无限重渲染排查:触发筛选致更新深度超限
问题描述
开发DoctorTable组件用于渲染带数据的表格及筛选功能,触发onSearchSubmit、onSearchClear或onUpdateFilter时出现无限重渲染错误(Maximum update depth exceeded),尝试用useRef存储uiData未解决。组件代码如下:
const DoctorTable = ({ onChange, value }: RichDoctorSelectProps) => { const [allDataSources, setAllDataSources] = useState< { page: number; items: TDataSource }[] >([]); const [uiData, setUiData] = useState<UIData>({ latitude, longitude, page: 1, search: undefined, specialityIds: [], city: undefined, zipcode: undefined, country: undefined, }); const { items: dataSource, refetch: refetchDoctorProjections, } = useDoctorProjections({ input: uiToInput(uiData), locale: Locale.DeDe, }); const onUpdateFilter = (filters: { address?: TAddressValue; specialityIds?: string[]; }) => { // filter logic here setUiData({ ...uiData, ...updatedFilters, page: 1, }); }; const onSearchSubmit = (search: string) => { setUiData((prevUiData) => ({ ...prevUiData, search, page: 1, })); // refetch data }; const onSearchClear = () => { setUiData({ ...uiData, search: undefined, page: 1, }); }; const noSearchAndNoFilters = !uiData.search && !uiData.specialityIds && !uiData.city && !uiData.country && !uiData.zipcode; const hasSearch = uiData.search; const hasFilters = (uiData.specialityIds || []).length > 0 || uiData.city || uiData.country || uiData.zipcode; useEffect(() => { if (uiData.page >= 1 && dataSource.length > 0) { const isPageAdded = allDataSources.some( (pageData) => pageData.page === uiData.page, ); if (hasFilters || hasSearch || noSearchAndNoFilters) { setAllDataSources((prevItems) => [ { page: 1, items: dataSource }, ]); } else if (!isPageAdded) { if (uiData.page === 1) { setAllDataSources([{ page: 1, items: dataSource }]); } else { setAllDataSources((prevItems) => [ ...prevItems, { page: uiData.page, items: dataSource }, ]); } } } }, [dataSource]); const table = ( <Table value={selectedDoctor} dataSource={allDataSources.flatMap((pageData) => pageData.items)} isLoading={loading && uiData.page === 1} isCalled={called} onRowClick={onChange} hasMore={uiData.page < total} onLoadMore={() => setUiData({ ...uiData, page: uiData.page + 1 })} /> ); const filters = ( <Filters searchValue={uiData.search} onSearchSubmit={onSearchSubmit} onSearchClear={onSearchClear} onUpdateFilter={onUpdateFilter} /> ); return ( <div className={cn.root}> {filters} {table} </div> ); }; export { DoctorTable };
此外,表格组件位于处理无限滚动的包装组件内。
可能原因及修复方案
1. useEffect依赖缺失导致循环更新
你的useEffect仅声明了dataSource为依赖,但内部使用了uiData、allDataSources、hasFilters、hasSearch、noSearchAndNoFilters这些状态或衍生值。依赖不全时,React无法准确判断何时触发useEffect,导致每次组件重渲染时useEffect都可能执行,而执行setAllDataSources又会触发重渲染,形成无限循环。
修复:
补全依赖数组,或将衍生值移到useEffect内部避免依赖外部变量:
useEffect(() => { // 将衍生判断逻辑移到内部,减少外部依赖 const noSearchAndNoFilters = !uiData.search && !uiData.specialityIds && !uiData.city && !uiData.country && !uiData.zipcode; const hasSearch = uiData.search; const hasFilters = (uiData.specialityIds || []).length > 0 || uiData.city || uiData.country || uiData.zipcode; if (uiData.page >= 1 && dataSource.length > 0) { const isPageAdded = allDataSources.some( (pageData) => pageData.page === uiData.page, ); if (hasFilters || hasSearch || noSearchAndNoFilters) { setAllDataSources((prevItems) => [ { page: 1, items: dataSource }, ]); } else if (!isPageAdded) { if (uiData.page === 1) { setAllDataSources([{ page: 1, items: dataSource }]); } else { setAllDataSources((prevItems) => [ ...prevItems, { page: uiData.page, items: dataSource }, ]); } } } }, [dataSource, uiData, allDataSources]); // 补全必要依赖
2. 回调函数未缓存导致子组件频繁重渲染
onUpdateFilter、onSearchSubmit、onSearchClear每次组件渲染都会创建新的函数实例,如果Filters组件未做缓存处理,会引发子组件频繁重渲染,甚至意外触发回调函数,进而触发uiData更新形成循环。
修复:
用useCallback缓存回调函数,并给Filters组件添加React.memo包裹:
// 缓存回调函数 const onUpdateFilter = useCallback((filters: { address?: TAddressValue; specialityIds?: string[]; }) => { // 补充filter逻辑,生成updatedFilters const updatedFilters = { ...(filters.address ? { city: filters.address.city, country: filters.address.country, zipcode: filters.address.zipcode } : {}), specialityIds: filters.specialityIds || [] }; // 使用函数式更新获取最新状态 setUiData(prev => ({ ...prev, ...updatedFilters, page: 1, })); }, []); const onSearchSubmit = useCallback((search: string) => { setUiData((prevUiData) => ({ ...prevUiData, search, page: 1, })); }, []); const onSearchClear = useCallback(() => { setUiData(prev => ({ ...prev, search: undefined, page: 1, })); }, []); // 给Filters组件添加React.memo缓存 const Filters = React.memo((props) => { // 原Filters组件逻辑 });
3. useDoctorProjections的input参数不稳定
uiToInput(uiData)每次渲染都会返回新对象,即使uiData值未变化,也会导致useDoctorProjections认为参数更新,重新发起请求,获取新的dataSource后触发useEffect执行setAllDataSources,形成循环。
修复:
用useMemo缓存uiToInput的结果,确保参数稳定:
const input = useMemo(() => uiToInput(uiData), [uiData]); const { items: dataSource, refetch: refetchDoctorProjections, } = useDoctorProjections({ input, locale: Locale.DeDe, });
4. 状态更新未使用函数式更新导致闭包问题
onUpdateFilter和onSearchClear中直接使用...uiData更新状态,可能因为闭包捕获旧的uiData值,导致状态更新异常,引发不必要的重渲染循环。
修复:
统一使用函数式更新获取最新状态值:
// onUpdateFilter中 setUiData(prev => ({ ...prev, ...updatedFilters, page: 1, })); // onSearchClear中 setUiData(prev => ({ ...prev, search: undefined, page: 1, }));
内容的提问来源于stack exchange,提问作者Mona

