如何在Jest/Vitest测试中更新React Prop?Timer组件测试问题
计时器组件测试问题:gameStatus更新无效的原因及解决方法
问题背景
组件代码
function Timer({ gameStatus }: TimerProps): JSX.Element { useEffect(() => { let interval; if (gameStatus) { interval = setInterval(() => setTimer((num) => num + 1), 1000); } else { if (interval) clearInterval(interval); } }, [gameStatus]); const [timer, setTimer] = useState(0); return <div role="timer">{convertToMMSSFormat(timer)}</div>; }
测试代码及报错
已验证gameStatus为true时计时器能正常启动,但测试停止逻辑时失败,测试代码如下:
it('stops counting when the game is over', () => { let gameStatus = true; const { getByRole } = render(<Timer gameStatus={gameStatus} />); act(() => vi.advanceTimersByTime(2000)); gameStatus = false; act(() => vi.advanceTimersByTime(2000)); const timer = getByRole('timer'); expect(timer).toHaveTextContent('00:02'); expect(timer).not.toHaveTextContent('00:04'); });
报错信息:
Error: expect(element).toHaveTextContent()
Expected element to have text content:
00:02
Received:
00:04
问题:手动修改gameStatus = false无法停止计数器,想知道原因及测试中更新Props的有效方法。
问题原因
- React组件的Props更新需要触发组件重新渲染,你测试中只是修改了普通JS变量
gameStatus的值,并没有通知React重新渲染Timer组件。 - 组件内的
useEffect依赖gameStatus,只有当组件接收到新的gameStatusProps时,才会重新执行该钩子的逻辑。你修改变量后组件没重新渲染,所以useEffect不会执行清理定时器的代码,计时器自然停不下来。
解决方法
1. 测试中用rerender更新Props
React Testing Library的render函数会返回rerender方法,用它可以重新渲染组件并传递新的Props。修改后的测试代码如下:
it('stops counting when the game is over', () => { let gameStatus = true; const { getByRole, rerender } = render(<Timer gameStatus={gameStatus} />); // 快进2秒,验证计时器走到00:02 act(() => vi.advanceTimersByTime(2000)); expect(getByRole('timer')).toHaveTextContent('00:02'); // 更新gameStatus并重新渲染组件 gameStatus = false; rerender(<Timer gameStatus={gameStatus} />); // 再快进2秒,验证计时器不再增长 act(() => vi.advanceTimersByTime(2000)); expect(getByRole('timer')).toHaveTextContent('00:02'); expect(getByRole('timer')).not.toHaveTextContent('00:04'); });
2. 修复组件内的定时器引用问题
原组件还有个隐藏问题:useEffect里的interval是局部变量,当gameStatus变为false时,这个变量是undefined,根本无法清理定时器。应该用useRef来保存定时器ID,确保清理时能拿到正确的引用:
function Timer({ gameStatus }: TimerProps): JSX.Element { const [timer, setTimer] = useState(0); const intervalRef = useRef<NodeJS.Timeout | null>(null); useEffect(() => { if (gameStatus) { // 把定时器ID存在ref里 intervalRef.current = setInterval(() => setTimer((num) => num + 1), 1000); } else { // 从ref里取ID清理定时器 if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } } // 组件卸载时也要清理定时器,防止内存泄漏 return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [gameStatus]); return <div role="timer">{convertToMMSSFormat(timer)}</div>; }
内容的提问来源于stack exchange,提问作者ElMoscaviador
相关产品推荐
相关产品推荐

