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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 11:19:51