RxJS引发无限循环:Maximum update depth exceeded错误排查求助
问题描述
我刚接手这个代码库,之前没使用过RxJS,但我认为问题根源在于依赖机制,而非RxJS本身。触发循环的代码位于SelectorColumn组件中,具体如下:
const { reduxSelector, unwrappedItems, mapper, emptyMessage } = hydrator( columnData.item, columnIndex, );
我不确定hydrator是否为RxJS中的概念,查资料没找到相关内容。我尝试回溯组件查找重渲染触发点,但因未发现明确关联的状态而陷入困境。
LocationSelector
export const LocationSelector = () => { const currentLocationId = 2; const dispatch = useAppDispatch(); const [locationSelectorState, setSelectorStateFromObservable] = useState< Map<ItemId, ItemCheckedStateTree> >(locationSelectorState$.getValue()); useEffect(() => { const subscription = locationSelectorState$.subscribe( setSelectorStateFromObservable, ); return () => subscription.unsubscribe(); }, []); const handleSelectorDone = (state: Map<ItemId, ItemCheckedStateTree>) => { setLocationSelectorState(state); setLocationSelectorVisible(false); }; const handleSelectorCancel = () => setLocationSelectorVisible(false); // TODO 选择器整体存在一个问题——它不具备响应式。 // 例如,如果LocationSelector组件的状态发生变化, // 这个hydrator不会被重新调用。这意味着选择器不会 // 重新渲染。这里我们已经做了临时处理,但逻辑上还是有个小问题, // 写一个'useSelectorHydrator'钩子会是更好的方案。 const hydrator = ( location?: SelectorItem, columnIndex?: number, ): SelectorHydrator => { const locationToSelectId = location?.id ?? currentLocationId; if (columnIndex === 0 && locationToSelectId) { dispatch( locationEndpoints.getLocationDetails.initiate({ locationId: locationToSelectId, }), ); return { reduxSelector: locationEndpoints.getLocationDetails.select({ locationId: locationToSelectId, }), mapper: locationDetailMapper, }; } const query = { immediateChildrenOnly: true, locationId: locationToSelectId ? +locationToSelectId : undefined, }; dispatch(locationEndpoints.getLocations.initiate(query)); return { reduxSelector: locationEndpoints.getLocations.select(query), mapper: locationMapper, }; }; return ( <Selector hydrator={hydrator} state={locationSelectorState} title="位置选择器" icon={faCrosshairs} onCancel={handleSelectorCancel} onDone={handleSelectorDone} /> ); };
MetricsBuildQuery
interface Props { isShowingResults: boolean; setShowingResults: Function; } const dateFormatString = "yyyy-MM-dd HH:mm:ss:SSS"; export const MetricsBuildQuery = ({ isShowingResults, setShowingResults, }: Props) => { const currentLocationId = 2; const dispatch = useAppDispatch(); const { data: locationDetail, error } = useGetLocationDetailsQuery({ locationId: currentLocationId, }); const [selectedLocations, setSelectedLocations] = useState<SelectorItem[]>( [], ); useEffect(() => { const subscription = selectedLocations$.subscribe(setSelectedLocations); return () => subscription.unsubscribe(); }, []); // 为API调用设置查询指标 ========================================= const [selectedPointIds, setSelectedPointIdsFromObservable] = useState< number[] >([]); useEffect(() => { const subscription = selectedPointIds$.subscribe( setSelectedPointIdsFromObservable, ); return () => subscription.unsubscribe(); }, []); // 预获取选中位置的资产 =========================== useEffect(() => { selectedLocations.forEach((option) => dispatch( assetsEndpoints.getAssets.initiate({ locationId: option?.id as number, }), ), ); setPointSelectorState(new Map()); }, [selectedLocations, dispatch]); // 设置第一个位置为选中状态 ============================================ useEffect(() => { if (currentLocationId && locationDetail) { const currentLocationSelectorItem = locationDetailMapper(locationDetail); setLocationSelectorState( createCheckedItem(currentLocationSelectorItem[0]), ); } }, [currentLocationId, locationDetail]); // 时间范围 ================================================================ const [timeRange, setTimeRange] = useState<TimeRange | undefined>(); useEffect(() => { const subscription = timeRange$.subscribe(setTimeRange); return () => subscription.unsubscribe(); }, []); // 位置选择器可见性 ============================================== const [locationSelectorVisible, setLocationSelectorVisible] = useState<boolean>(false); useEffect(() => { const subscription = locationSelectorVisible$.subscribe( setLocationSelectorVisible, ); return () => subscription.unsubscribe(); }, []); // 点位选择器可见性 ================================================= const [pointSelectorVisible, setPointSelectorVisible] = useState<boolean>(false); useEffect(() => { const subscription = pointSelectorVisible$.subscribe( setPointSelectorVisible, ); return () => subscription.unsubscribe(); }, []); const [hasPoints, setHasPoints] = useState(false); useEffect(() => { const subscription = hasPoints$.subscribe(setHasPoints); return () => subscription.unsubscribe(); }, []); const handleRunQuery = () => { setPointQueryPointIds(selectedPointIds.join(",")); if (timeRange) { setPointQueryTimeRange( format(timeRange.customStartTime, dateFormatString), format(timeRange.customEndTime, dateFormatString), ); } setShowingResults(true); }; const ChooseLocationsMemo = React.memo(ChooseLocations); const PointSelectorMemo = React.memo(PointSelector); const LocationSelectorMemo = React.memo(LocationSelector); return ( <> {/* TODO 需要缓存这些组件 */} {locationSelectorVisible && <LocationSelectorMemo />} {pointSelectorVisible && <PointSelectorMemo />} <Paper noPadding={true} header={ <PaperHeader title="构建查询" icon={CalendarTodayOutlinedIcon} toolbar={ <Stack direction="row" justifyContent="flex-end" spacing={1}> <Button onClick={() => handleRunQuery()} disabled={!hasPoints}> {isShowingResults ? "重新执行查询" : "执行查询"} </Button> </Stack> } /> } > <div tw="flex m-4 gap-6"> <div tw="flex flex-col gap-4 flex-1"> {/* TODO 需要缓存这些组件 */} <ChooseRangeOption /> <ChooseLocationsMemo /> <ChoosePoints /> </div> <ChooseRange /> </div> </Paper> </> ); };
SelectorColumn
const dummySelector = createSelector(() => null); const defaultStyle = { width: 224, height: 400, }; export const SelectorColumn = ({ hydrator, onItemsFetched, checkedState, onDilateItem, onCheckedChange, columnIndex, columnData, highlightedItemId, leavesOnly, }: Props) => { const [items, setItems] = useState<SelectorItem[]>([]); const [itemsAvailable, setItemsAvailable] = useState(false); const [filterInput, setFilterInput] = useState(""); const filteredList = useMemo( () => { return items.filter((item) => item.description .toLocaleLowerCase() .includes(filterInput.toLocaleLowerCase()), )}, [items, filterInput], ); let isLoading = !itemsAvailable; let data; const [springs, api] = useSpring(() => { return { from: { left: columnIndex === 0 ? 0 : -defaultStyle.width }, config: { duration: 200, }, }}); useEffect(() => { if (!isLoading && items.length) { api.start({ to: { left: 0 } }); } }, [isLoading, items]); const { reduxSelector, unwrappedItems, mapper, emptyMessage } = hydrator( columnData.item, columnIndex, ); const selectorResult = useAppSelector(reduxSelector ?? dummySelector) as { isLoading: boolean; data: any; }; if (selectorResult && isLoading) { isLoading = selectorResult.isLoading; data = selectorResult.data; } if (unwrappedItems) { if (unwrappedItems.length && !itemsAvailable) { setItems(unwrappedItems); setItemsAvailable(true); onItemsFetched(unwrappedItems); } } else if (data && !itemsAvailable) { const fetchedItems = mapper ? mapper(data) : data || []; // 如果data未定义,回退为空数组 setItems(fetchedItems); setItemsAvailable(true); onItemsFetched(fetchedItems); } const transRef = useSpringRef(); useEffect(() => { if (!isLoading) { transRef.start(); } }, [isLoading]); const renderSelectorItem = (props: ListChildComponentProps) => { const { index, style } = props; const selectorItem = filteredList[index]; // 这里用item ID比用索引更安全吗? // 其实索引和ID一样唯一,所以可能没区别 const handleCheckboxChange = () => { onCheckedChange(columnIndex, selectorItem); }; return ( <div key={selectorItem.id} css={[ tw`flex h-10 justify-between items-center px-4 border-b border-component-dark-1 w-full gap-1`, highlightedItemId === selectorItem.id && tw`bg-ctrl-bg`, ]} style={style} > <div tw="flex gap-8 w-full overflow-hidden"> <Checkbox label={selectorItem.description} checked={!!checkedState?.get(selectorItem.id)} indeterminate={checkedState?.get(selectorItem.id) === CheckedState.Indeterminate} onChange={handleCheckboxChange} disabled={leavesOnly && !!selectorItem.childIds} /> </div> <div> {selectorItem.childIds?.length && ( <FontAwesomeIcon icon={faAngleRight} tw="text-black text-sm cursor-pointer" onClick={() => onDilateItem(selectorItem)} /> )} </div> </div> ); }; const handleFilterChange = (event: ChangeEvent<HTMLInputElement>) => { setFilterInput(event.target.value || ""); // 如果值未定义或为空,回退为空字符串 }; const handleFilterClearClicked = () => { setFilterInput(""); }; return ( <div tw="relative overflow-hidden h-[400px] w-[224px] border-r border-component-dark-3"> {isLoading ? ( <div tw="flex items-center justify-center h-full w-full"> <Spinner /> </div> ) : items.length ? ( <div tw="flex flex-col w-full h-full"> <div tw="grow"> <animated.div tw="absolute top-0 left-0" css={[tw`absolute h-full shadow-selector`]} id="test" style={springs} > <FixedSizeList height={355} width={defaultStyle.width} itemSize={46} itemCount={filteredList.length} overscanCount={3} > {/* // @ts-ignore */} {renderSelectorItem} </FixedSizeList> </animated.div> </div> <SelectorFilterInput value={filterInput} onChange={handleFilterChange} onClearClicked={handleFilterClearClicked} /> </div> ) : ( <div tw="flex items-center justify-center h-full w-full"> <Text variant="noResult" tw="w-7/12 text-center"> {emptyMessage} </Text> </div> )} </div> ); };
内容的提问来源于stack exchange,提问作者Jamie Hutber
相关产品推荐
相关产品推荐

