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

@testing-library/react unmount失效,useEffect清理函数未执行求助

@testing-library/react unmount未触发useEffect清理函数的问题排查与解决

我在使用@testing-library/react的unmount方法时遇到了失效问题——调用unmount后,组件中useEffect的返回清理函数并未执行(控制台未打印unsubscribing日志),导致测试中emitter.off的调用次数不符合预期。

组件代码

React.useEffect(() => {
  emitter.on(MY_EVENT, callback);

  return () => { 
    console.log('unsubscribing'); // 未打印该日志
    emitter.off(MY_EVENT, callback);
  };
}, []);

测试代码

it.only('should add and remove event listeners', () => {
  const { unmount } = render(<Component {...defaultProps} />);
  expect(emitter.on).toHaveBeenCalledTimes(1);
  expect(observers[MY_EVENT]).toBeDefined();

  unmount();
  expect(emitter.off).toHaveBeenCalledTimes(1);
  expect(observers[MY_EVENT]).toBeUndefined();
});

测试报错信息

expect(jest.fn()).toHaveBeenCalledTimes(expected)

Expected number of calls: 1
Received number of calls: 0

  23 |
  24 |     unmount();
> 25 |     expect(emitter.off).toHaveBeenCalledTimes(1);
      |                         ^
  26 |     expect(observers[MY_EVENT]).toBeUndefined();

依赖版本

"react": "17.0.2",
"react-dom": "17.0.2",
"@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2"

排查与解决方案

1. 固定回调函数的引用一致性

如果callback是组件内部定义的普通函数,每次组件渲染都会生成新的引用。即使useEffect依赖为空数组,若组件存在意外重渲染,会导致emitter.on绑定的回调与emitter.off解绑的回调不是同一个引用,看似清理函数未执行(实际解绑失败)。用useCallback固定引用:

const callback = React.useCallback(() => {
  // 你的回调逻辑
}, []);

React.useEffect(() => {
  emitter.on(MY_EVENT, callback);
  return () => {
    console.log('unsubscribing');
    emitter.off(MY_EVENT, callback);
  };
}, [callback]);

2. 手动调用react-dom的卸载API

绕过@testing-library/react的unmount函数,直接用react-dom的unmountComponentAtNode手动卸载组件,验证是否能触发清理:

import { unmountComponentAtNode } from 'react-dom';

it.only('should add and remove event listeners', () => {
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  render(<Component {...defaultProps} />, { container });
  
  expect(emitter.on).toHaveBeenCalledTimes(1);
  expect(observers[MY_EVENT]).toBeDefined();

  unmountComponentAtNode(container);
  
  expect(emitter.off).toHaveBeenCalledTimes(1);
  expect(observers[MY_EVENT]).toBeUndefined();
});

3. 排查组件卸载时的隐藏错误

组件卸载过程中如果抛出异常,会中断清理函数的执行。可以在测试中监听console.error,排查是否有未捕获的错误:

beforeEach(() => {
  jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
  console.error.mockRestore();
});

it.only('should add and remove event listeners', () => {
  const { unmount } = render(<Component {...defaultProps} />);
  expect(emitter.on).toHaveBeenCalledTimes(1);
  
  unmount();
  
  expect(console.error).not.toHaveBeenCalled();
  
  expect(emitter.off).toHaveBeenCalledTimes(1);
  expect(observers[MY_EVENT]).toBeUndefined();
});

4. 验证emitter的mock逻辑

如果emitter是用jest mock的,确保on和off的mock没有拦截或篡改正常调用:

jest.mock('./path-to-emitter', () => ({
  on: jest.fn(),
  off: jest.fn(),
  // 其他需要mock的属性
}));

内容的提问来源于stack exchange,提问作者Bhushan Laware

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 19:05:20