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

如何使用RTL的rerender测试useEffect内部的动作?

问题场景与困惑

我有如下React组件:

const Example = ({ prop1, prop2 }) => {
  useEffect(() => {
    // ...其他逻辑
    someFunctionCall(arg)
  }, [prop1])

  useEffect(() => {
    // ...其他逻辑
    someFunctionCall(arg)
  }, [prop2])

  // ...组件其他内容
}

我想要测试:当修改prop1时,仅触发一次新的someFunctionCall调用。于是写了如下测试代码:

it('when prop1 changes', () => {
  const { rerender } = render(<Example prop1='value1' prop2='value2' />)

  // 首次渲染时两个useEffect都会执行
  expect(someFunctionCall).toBeCalledTimes(2)

  rerender(<Example prop1='newValue' prop2='value2' />)

  // 预期prop1更新时仅第一个useEffect执行,累计调用3次
  expect(someFunctionCall).toBeCalledTimes(3)
})

但测试失败了:函数被调用了4次。查了原因发现,RTL的rerender并非只更新props,而是用新props重新渲染整个组件,导致两个useEffect再次执行,和真实系统的props更新行为不符。请问该如何用RTL正确测试这种props变更的功能?

解决方案

1. 重置Mock调用计数(最直接的解决方式)

问题出在你一直在累计统计someFunctionCall的调用次数,正确做法是在rerender前清空mock的历史调用记录,只统计props变更后的调用次数。修改后的测试代码如下:

it('当prop1变更时,仅触发一次新的函数调用', () => {
  const { rerender } = render(<Example prop1='value1' prop2='value2' />)

  // 验证首次渲染的调用次数
  expect(someFunctionCall).toBeCalledTimes(2)

  // 清空mock的历史调用记录,只关注后续rerender的行为
  someFunctionCall.mockClear()

  rerender(<Example prop1='newValue' prop2='value2' />)

  // 此时仅依赖prop1的useEffect会执行,所以调用次数为1
  expect(someFunctionCall).toBeCalledTimes(1)
})

2. 排查组件逻辑是否存在不稳定依赖

需要确认:你的组件本身逻辑是否符合预期。RTL的rerender本质是完全模拟React的props更新流程,和真实场景下的props更新行为一致。如果出现两个useEffect都重新执行的情况,很可能是arg存在不稳定依赖——比如arg是每次渲染都会重新创建的对象、数组或函数,导致即使prop没有变化,useEffect也会被触发。

这种情况下需要用useCallback或useMemo缓存arg,确保依赖数组的稳定性,比如:

const Example = ({ prop1, prop2 }) => {
  // 用useMemo缓存arg,避免每次渲染重新创建
  const arg = useMemo(() => ({ /* 你的arg内容 */ }), [])

  useEffect(() => {
    someFunctionCall(arg)
  }, [prop1, arg])

  useEffect(() => {
    someFunctionCall(arg)
  }, [prop2, arg])

  // ...组件其他内容
}

3. 更细致的调用验证(可选)

如果需要确认rerender后的调用确实来自依赖prop1的useEffect,可以通过mock的mock.calls数组验证调用参数,或者在useEffect中传入不同的标识参数:

组件中修改:

useEffect(() => {
  someFunctionCall('prop1-triggered', arg)
}, [prop1])

useEffect(() => {
  someFunctionCall('prop2-triggered', arg)
}, [prop2])

测试中验证:

// rerender后检查调用参数
expect(someFunctionCall).toHaveBeenCalledWith('prop1-triggered', expect.anything())
// 确保没有来自prop2的调用
expect(someFunctionCall).not.toHaveBeenCalledWith('prop2-triggered', expect.anything())

内容的提问来源于stack exchange,提问作者Ben Graham

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 11:11:57