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

使用Jest测试React中RxJS订阅超时如何正确取消订阅

问题根因
  • 测试仅完成了组件渲染和快照校验,没有主动触发组件卸载,useEffect中返回的退订逻辑从未执行,rxjs的timer订阅持续持有事件循环引用,导致Jest无法正常结束进程。
  • 组件内的timerStart$、timerStop$、timer$直接写在组件函数体中,每次组件重渲染都会生成新的实例,旧实例的订阅无法被正常清理,存在固有内存泄漏隐患。
  • handleSliderItemClick中创建的timer(delay)没有调用subscribe也没有对应清理逻辑,属于隐藏的泄漏点。
  • 原测试用例未传入必填proponActiveItemChange,也可能引发意料之外的运行时错误。
修复步骤

1. 调整组件内部逻辑,完善订阅管理

把rxjs相关实例用useRef存储,避免重渲染丢失引用,同时补全所有订阅的清理逻辑:

import React, { useEffect, useState, useRef } from "react";
import { ISlide } from "./ISlide";
import styled from "styled-components";
import { BehaviorSubject, Subject, timer, Subscription } from "rxjs";
import { repeatWhen, take, takeUntil, takeWhile, tap } from "rxjs/operators";

interface ISliderProps {
  slides: ISlide[];
  activeSlide?: number;
  onActiveItemChange: (index: number) => void;
  itemsPerPage?: number;
  delay?: number;
  interval?: number;
  loop?: boolean;
}

const SliderWrapper = styled.ul`
  /* 原有样式 */
`;

const SliderItem = styled.li<{active: boolean}>`
  /* 原有样式 */
`;

export const Slider = (props: ISliderProps) => {
  const {
    slides,
    activeSlide = 0,
    onActiveItemChange,
    itemsPerPage = 1,
    delay = 5000,
    interval = 5000,
    loop = true
  } = props;

  const [currentActiveSlide, setCurrentActiveSlide] = useState(activeSlide);
  const sliderCount = Math.ceil(slides.length / itemsPerPage);

  // 用useRef存储rxjs实例,避免重渲染丢失
  const timerStart$ = useRef(new BehaviorSubject(null)).current;
  const timerStop$ = useRef(new Subject<null>()).current;
  // 存储所有订阅,统一清理
  const subscriptions = useRef(new Subscription()).current;

  const timer$ = timer(delay, interval).pipe(
    takeWhile(() => loop),
    takeUntil(timerStop$),
    repeatWhen(() => timerStart$)
  );

  useEffect(() => {
    const timerSubscription = timer$.subscribe({
      next: (i) => {
        const nextSlide = i >= sliderCount ? i % sliderCount : i;
        onActiveItemChange(nextSlide);
        setCurrentActiveSlide(nextSlide);
      }
    });
    // 把订阅加入统一管理
    subscriptions.add(timerSubscription);

    // 卸载时清理所有订阅,同时complete掉subject
    return () => {
      subscriptions.unsubscribe();
      timerStop$.complete();
      timerStart$.complete();
    };
  // 依赖补全,参数变更时重新初始化订阅
  }, [loop, delay, interval, sliderCount, onActiveItemChange]);

  const handleSliderItemClick = (i: number) => {
    setCurrentActiveSlide(i);
    timerStop$.next(null);
    // 补全订阅并加入统一管理
    const clickTimerSub = timer(delay).pipe(
      take(1),
      tap(() => timerStart$.next(null))
    ).subscribe();
    subscriptions.add(clickTimerSub);
  };

  return (
    <>
      <SliderWrapper>
        {currentActiveSlide}
        {slides.map(({ id }, i) =>
          <SliderItem
            key={id}
            aria-label={"scroll page number " + id}
            onClick={() => handleSliderItemClick(i)}
            active={i === currentActiveSlide}
          />)}
      </SliderWrapper>
    </>
  );
};

2. 调整测试用例,主动卸载组件

测试完成后调用unmount触发组件清理逻辑,也可以搭配假定时器进一步提升测试效率,无需使用异步done回调:

import React from "react";
import { Slider } from "../../../components/slider/Slider";
import renderer, { act } from "react-test-renderer";
import { ISlide } from "../../../components/slider/ISlide";

it("renders without crashing", () => {
  // 启用假定时器,避免真实定时器占用事件循环
  jest.useFakeTimers();
  const slides: ISlide[] = [
    { id: 1 }, { id: 2 }, { id: 3 },
    { id: 4 }, { id: 5 }, { id: 6 },
    { id: 7 }, { id: 8 }
  ];

  let component;
  act(() => {
    component = renderer.create(<Slider 
      itemsPerPage={3} 
      slides={slides} 
      activeSlide={0} 
      onActiveItemChange={() => {}} 
    />);
    // 执行所有待运行的定时器
    jest.runAllTimers();
  });
  
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // 主动卸载组件,触发清理逻辑
  act(() => {
    component.unmount();
  });

  // 恢复定时器
  jest.useRealTimers();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.13 23:12:02